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::data_type::DataType;
35use datatypes::types::json_type::JsonNativeType;
36use futures::stream::BoxStream;
37use session::context::{QueryContext, QueryContextRef};
38use snafu::ResultExt;
39use store_api::metadata::RegionMetadataRef;
40use store_api::region_engine::RegionEngineRef;
41use store_api::storage::{RegionId, ScanRequest, TimeSeriesDistribution, TimeSeriesRowSelector};
42use table::TableRef;
43use table::metadata::{TableId, TableInfoRef};
44use table::table::adapter::{dictionary_encode_string_columns, supports_pk_dictionary_encoding};
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 register_catalog(
73        &self,
74        _name: String,
75        _catalog: Arc<dyn CatalogProvider>,
76    ) -> Option<Arc<dyn CatalogProvider>> {
77        None
78    }
79
80    fn catalog_names(&self) -> Vec<String> {
81        vec![]
82    }
83
84    fn catalog(&self, _name: &str) -> Option<Arc<dyn CatalogProvider>> {
85        Some(Arc::new(self.catalog.clone()))
86    }
87}
88
89/// A dummy catalog provider for [DummyCatalogList].
90#[derive(Clone, Debug)]
91struct DummyCatalogProvider {
92    schema: DummySchemaProvider,
93}
94
95impl CatalogProvider for DummyCatalogProvider {
96    fn schema_names(&self) -> Vec<String> {
97        vec![]
98    }
99
100    fn schema(&self, _name: &str) -> Option<Arc<dyn SchemaProvider>> {
101        Some(Arc::new(self.schema.clone()))
102    }
103}
104
105/// A dummy schema provider for [DummyCatalogList].
106#[derive(Clone, Debug)]
107struct DummySchemaProvider {
108    table: Arc<dyn TableProvider>,
109}
110
111#[async_trait]
112impl SchemaProvider for DummySchemaProvider {
113    fn table_names(&self) -> Vec<String> {
114        vec![]
115    }
116
117    async fn table(
118        &self,
119        _name: &str,
120    ) -> datafusion::error::Result<Option<Arc<dyn TableProvider>>> {
121        Ok(Some(self.table.clone()))
122    }
123
124    fn table_exist(&self, _name: &str) -> bool {
125        true
126    }
127}
128
129/// For [TableProvider] and [DummyCatalogList]
130#[derive(Clone)]
131pub struct DummyTableProvider {
132    region_id: RegionId,
133    engine: RegionEngineRef,
134    metadata: RegionMetadataRef,
135    /// Keeping a mutable request makes it possible to change in the optimize phase.
136    scan_request: Arc<Mutex<ScanRequest>>,
137    query_ctx: Option<QueryContextRef>,
138}
139
140impl fmt::Debug for DummyTableProvider {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.debug_struct("DummyTableProvider")
143            .field("region_id", &self.region_id)
144            .field("metadata", &self.metadata)
145            .field("scan_request", &self.scan_request)
146            .finish()
147    }
148}
149
150#[async_trait]
151impl TableProvider for DummyTableProvider {
152    fn schema(&self) -> SchemaRef {
153        let schema = self.metadata.schema.arrow_schema();
154        if !supports_pk_dictionary_encoding(self.engine.name()) {
155            return schema.clone();
156        }
157        dictionary_encode_string_columns(schema, |index| {
158            self.metadata.column_metadatas[index].semantic_type == SemanticType::Tag
159        })
160    }
161
162    fn table_type(&self) -> TableType {
163        TableType::Base
164    }
165
166    async fn scan(
167        &self,
168        _state: &dyn Session,
169        projection: Option<&Vec<usize>>,
170        filters: &[Expr],
171        limit: Option<usize>,
172    ) -> datafusion::error::Result<Arc<dyn ExecutionPlan>> {
173        let mut request = self.scan_request.lock().unwrap().clone();
174        request.projection = projection.cloned();
175        request.filters = filters.to_vec();
176        request.limit = limit;
177        if let Some(query_ctx) = &self.query_ctx {
178            let is_sink_scan = is_sink_scan(query_ctx, self.region_id)
179                .map_err(|e| DataFusionError::External(Box::new(e)))?;
180            apply_cached_snapshot_to_request(query_ctx, self.region_id, is_sink_scan, &mut request);
181        }
182
183        let scanner = self
184            .engine
185            .handle_query(self.region_id, request.clone())
186            .await
187            .map_err(|e| DataFusionError::External(Box::new(e)))?;
188
189        if request.snapshot_on_scan
190            && let Some(query_ctx) = &self.query_ctx
191            && let Some(snapshot_sequence) = scanner.snapshot_sequence()
192        {
193            bind_snapshot_bound_region_seq(query_ctx, self.region_id, snapshot_sequence)
194                .map_err(|e| DataFusionError::External(Box::new(e)))?;
195        }
196
197        let query_memory_tracker = self.engine.query_memory_tracker();
198        let mut scan_exec = RegionScanExec::new(scanner, request, query_memory_tracker)?;
199        if let Some(query_ctx) = &self.query_ctx {
200            scan_exec.set_explain_verbose(query_ctx.explain_verbose());
201        }
202        Ok(Arc::new(scan_exec))
203    }
204
205    fn supports_filters_pushdown(
206        &self,
207        filters: &[&Expr],
208    ) -> datafusion::error::Result<Vec<TableProviderFilterPushDown>> {
209        let supported = filters
210            .iter()
211            .map(|e| {
212                // Simple filter on primary key columns are precisely evaluated.
213                if let Some(simple_filter) =
214                    SimpleFilterEvaluator::try_new_with_column_type(e, &|name| {
215                        self.metadata
216                            .column_by_name(name)
217                            .map(|column| column.column_schema.data_type.as_arrow_type())
218                    })
219                {
220                    if self
221                        .metadata
222                        .column_by_name(simple_filter.column_name())
223                        .and_then(|c| {
224                            (c.semantic_type == SemanticType::Tag
225                                || c.semantic_type == SemanticType::Timestamp)
226                                .then_some(())
227                        })
228                        .is_some()
229                    {
230                        TableProviderFilterPushDown::Exact
231                    } else {
232                        TableProviderFilterPushDown::Inexact
233                    }
234                } else {
235                    TableProviderFilterPushDown::Inexact
236                }
237            })
238            .collect();
239        Ok(supported)
240    }
241}
242
243impl DummyTableProvider {
244    /// Creates a new provider.
245    pub fn new(region_id: RegionId, engine: RegionEngineRef, metadata: RegionMetadataRef) -> Self {
246        let preserve_pk_dictionary_encoding = supports_pk_dictionary_encoding(engine.name());
247        Self {
248            region_id,
249            engine,
250            metadata,
251            scan_request: Arc::new(Mutex::new(ScanRequest {
252                preserve_pk_dictionary_encoding,
253                ..Default::default()
254            })),
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    /// Clones this provider for one logical table-scan use-site.
279    ///
280    /// The optimizer may attach different hints to scans that share the same
281    /// catalog provider, so the per-scan request must not share its mutex.
282    pub fn clone_for_scan(&self) -> Self {
283        Self {
284            scan_request: Arc::new(Mutex::new(self.scan_request.lock().unwrap().clone())),
285            ..self.clone()
286        }
287    }
288
289    pub fn with_sequence(&self, sequence: u64) {
290        self.scan_request.lock().unwrap().memtable_max_sequence = Some(sequence);
291    }
292
293    pub(crate) fn with_json_type_hint(&self, hint: HashMap<String, JsonNativeType>) {
294        self.scan_request.lock().unwrap().json_type_hint = hint;
295    }
296
297    /// Gets the scan request of the provider.
298    #[cfg(test)]
299    pub fn scan_request(&self) -> ScanRequest {
300        self.scan_request.lock().unwrap().clone()
301    }
302}
303
304pub struct DummyTableProviderFactory;
305
306impl DummyTableProviderFactory {
307    pub async fn create_table_provider(
308        &self,
309        region_id: RegionId,
310        engine: RegionEngineRef,
311        query_ctx: Option<QueryContextRef>,
312    ) -> Result<DummyTableProvider> {
313        let metadata =
314            engine
315                .get_metadata(region_id)
316                .await
317                .with_context(|_| GetRegionMetadataSnafu {
318                    engine: engine.name(),
319                    region_id,
320                })?;
321
322        let mut scan_request = if let Some(ctx) = query_ctx.as_ref() {
323            scan_request_from_query_context(region_id, ctx)?
324        } else {
325            ScanRequest::default()
326        };
327        scan_request.preserve_pk_dictionary_encoding =
328            supports_pk_dictionary_encoding(engine.name());
329
330        Ok(DummyTableProvider {
331            region_id,
332            engine,
333            metadata,
334            scan_request: Arc::new(Mutex::new(scan_request)),
335            query_ctx,
336        })
337    }
338}
339
340fn scan_request_from_query_context(
341    region_id: RegionId,
342    query_ctx: &QueryContext,
343) -> Result<ScanRequest> {
344    let decision = decide_flow_scan(query_ctx, region_id)?;
345    Ok(build_scan_request(query_ctx, region_id, &decision))
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
349struct FlowScanDecision {
350    /// Whether this region is the flow sink-table scan.
351    /// Sink scans intentionally bypass incremental and snapshot-binding semantics.
352    is_sink_scan: bool,
353    /// Whether this scan should bind a memtable upper bound when opening the scan.
354    /// This is only the initial intent; if a cached bound already exists in `query_ctx`,
355    /// we reuse that cached bound instead and clear this flag.
356    snapshot_on_scan: bool,
357    /// Optional lower exclusive memtable sequence bound for incremental reads.
358    /// When set, only rows with sequence strictly greater than this bound are read from memtables.
359    memtable_min_sequence: Option<u64>,
360    /// Optional cached per-region snapshot already bound in `query_ctx`.
361    /// When present, this becomes the effective memtable upper bound and suppresses
362    /// binding a new snapshot on scan open.
363    memtable_max_sequence: Option<u64>,
364    /// Whether to skip SST files for memtable-only incremental source scans.
365    skip_sst_files: bool,
366    /// Whether this source scan must enforce the exact sequence range.
367    exact_sequence_range: bool,
368}
369
370impl FlowScanDecision {
371    fn plain_scan() -> Self {
372        Self {
373            is_sink_scan: true,
374            snapshot_on_scan: false,
375            memtable_min_sequence: None,
376            memtable_max_sequence: None,
377            skip_sst_files: false,
378            exact_sequence_range: false,
379        }
380    }
381}
382
383fn decide_flow_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<FlowScanDecision> {
384    let Some(flow_extensions) =
385        FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())?
386    else {
387        return Ok(FlowScanDecision {
388            is_sink_scan: false,
389            snapshot_on_scan: false,
390            memtable_min_sequence: None,
391            memtable_max_sequence: query_ctx.get_snapshot(region_id.as_u64()),
392            skip_sst_files: false,
393            exact_sequence_range: false,
394        });
395    };
396
397    // Sink-table scans intentionally bypass all flow scan semantics. They should
398    // behave like plain reads and must not participate in incremental lower bounds
399    // or per-region snapshot binding/reuse.
400    if flow_extensions.sink_table_id == Some(region_id.table_id()) {
401        return Ok(FlowScanDecision::plain_scan());
402    }
403
404    let apply_incremental = flow_extensions.validate_for_scan(region_id)?;
405
406    let memtable_min_sequence = if apply_incremental {
407        flow_extensions
408            .incremental_after_seqs
409            .as_ref()
410            .and_then(|seqs| seqs.get(&region_id.as_u64()))
411            .copied()
412    } else {
413        None
414    };
415
416    let memtable_max_sequence = query_ctx.get_snapshot(region_id.as_u64());
417
418    // `skip_sst_files` is only valid for memtable-only incremental deltas,
419    // identified by a lower checkpoint bound. A snapshot upper bound without an
420    // incremental lower bound is a fenced full-snapshot read and must keep SSTs
421    // in the scan so mito can reject stale upper bounds after H has been flushed
422    // into SSTs, instead of silently bypassing the stale-fence check. If a future
423    // incremental delta also carries an upper bound, the lower-bound stale check
424    // still proves whether memtable-only is safe.
425    let skip_sst_files = apply_incremental
426        && memtable_min_sequence.is_some()
427        && flow_extensions.incremental_mode == Some(FlowIncrementalMode::MemtableOnly);
428
429    Ok(FlowScanDecision {
430        is_sink_scan: false,
431        snapshot_on_scan: memtable_max_sequence.is_none()
432            && flow_extensions.should_collect_region_watermark(),
433        memtable_min_sequence,
434        memtable_max_sequence,
435        skip_sst_files,
436        exact_sequence_range: apply_incremental
437            && flow_extensions.incremental_mode == Some(FlowIncrementalMode::SequenceRange),
438    })
439}
440
441fn build_scan_request(
442    query_ctx: &QueryContext,
443    region_id: RegionId,
444    decision: &FlowScanDecision,
445) -> ScanRequest {
446    // Build the initial scan request from the final decision known at provider creation
447    // time. A later scan may still refresh `memtable_max_sequence` if another source scan
448    // has bound a snapshot into `query_ctx` after this provider was created.
449    ScanRequest {
450        sst_min_sequence: (!decision.is_sink_scan && !decision.exact_sequence_range)
451            .then(|| query_ctx.sst_min_sequence(region_id.as_u64()))
452            .flatten(),
453        skip_sst_files: decision.skip_sst_files,
454        snapshot_on_scan: decision.snapshot_on_scan,
455        exact_sequence_range: decision.exact_sequence_range,
456        memtable_min_sequence: decision.memtable_min_sequence,
457        memtable_max_sequence: decision.memtable_max_sequence,
458        ..Default::default()
459    }
460}
461
462fn is_sink_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<bool> {
463    Ok(
464        FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())?
465            .is_some_and(|exts| exts.sink_table_id == Some(region_id.table_id())),
466    )
467}
468
469fn apply_cached_snapshot_to_request(
470    query_ctx: &QueryContext,
471    region_id: RegionId,
472    is_sink_scan: bool,
473    scan_request: &mut ScanRequest,
474) {
475    if is_sink_scan {
476        return;
477    }
478
479    if let Some(snapshot_sequence) = query_ctx.get_snapshot(region_id.as_u64()) {
480        // Reuse the previously bound per-region snapshot instead of rebinding a new
481        // upper bound on scan open. This refresh is still needed at scan time because
482        // the provider's cached request may have been built before another source scan
483        // bound the shared query-level snapshot into `query_ctx`.
484        scan_request.memtable_max_sequence = Some(snapshot_sequence);
485        scan_request.snapshot_on_scan = false;
486    }
487}
488
489fn bind_snapshot_bound_region_seq(
490    query_ctx: &QueryContext,
491    region_id: RegionId,
492    snapshot_sequence: u64,
493) -> Result<u64> {
494    if let Some(existing) = query_ctx.get_snapshot(region_id.as_u64()) {
495        if existing != snapshot_sequence {
496            return crate::error::ConflictingSnapshotSequenceSnafu {
497                region_id,
498                existing,
499                new: snapshot_sequence,
500            }
501            .fail();
502        }
503        Ok(existing)
504    } else {
505        query_ctx.set_snapshot(region_id.as_u64(), snapshot_sequence);
506        Ok(snapshot_sequence)
507    }
508}
509
510#[async_trait]
511impl TableProviderFactory for DummyTableProviderFactory {
512    async fn create(
513        &self,
514        region_id: RegionId,
515        engine: RegionEngineRef,
516        ctx: Option<QueryContextRef>,
517    ) -> Result<Arc<dyn TableProvider>> {
518        let provider = self.create_table_provider(region_id, engine, ctx).await?;
519        Ok(Arc::new(provider))
520    }
521}
522
523#[async_trait]
524pub trait TableProviderFactory: Send + Sync {
525    async fn create(
526        &self,
527        region_id: RegionId,
528        engine: RegionEngineRef,
529        ctx: Option<QueryContextRef>,
530    ) -> Result<Arc<dyn TableProvider>>;
531}
532
533pub type TableProviderFactoryRef = Arc<dyn TableProviderFactory>;
534
535/// A dummy catalog manager that always returns empty results.
536///
537/// Used to fill the arg of `QueryEngineFactory::new_with_plugins` in datanode.
538pub struct DummyCatalogManager;
539
540impl DummyCatalogManager {
541    /// Returns a new `CatalogManagerRef` instance.
542    pub fn arc() -> CatalogManagerRef {
543        Arc::new(Self)
544    }
545}
546
547#[async_trait::async_trait]
548impl CatalogManager for DummyCatalogManager {
549    fn as_any(&self) -> &dyn Any {
550        self
551    }
552
553    async fn catalog_names(&self) -> CatalogResult<Vec<String>> {
554        Ok(vec![])
555    }
556
557    async fn schema_names(
558        &self,
559        _catalog: &str,
560        _query_ctx: Option<&QueryContext>,
561    ) -> CatalogResult<Vec<String>> {
562        Ok(vec![])
563    }
564
565    async fn table_names(
566        &self,
567        _catalog: &str,
568        _schema: &str,
569        _query_ctx: Option<&QueryContext>,
570    ) -> CatalogResult<Vec<String>> {
571        Ok(vec![])
572    }
573
574    async fn catalog_exists(&self, _catalog: &str) -> CatalogResult<bool> {
575        Ok(false)
576    }
577
578    async fn schema_exists(
579        &self,
580        _catalog: &str,
581        _schema: &str,
582        _query_ctx: Option<&QueryContext>,
583    ) -> CatalogResult<bool> {
584        Ok(false)
585    }
586
587    async fn table_exists(
588        &self,
589        _catalog: &str,
590        _schema: &str,
591        _table: &str,
592        _query_ctx: Option<&QueryContext>,
593    ) -> CatalogResult<bool> {
594        Ok(false)
595    }
596
597    async fn table(
598        &self,
599        _catalog: &str,
600        _schema: &str,
601        _table_name: &str,
602        _query_ctx: Option<&QueryContext>,
603    ) -> CatalogResult<Option<TableRef>> {
604        Ok(None)
605    }
606
607    async fn table_id(
608        &self,
609        _catalog: &str,
610        _schema: &str,
611        _table_name: &str,
612        _query_ctx: Option<&QueryContext>,
613    ) -> CatalogResult<Option<TableId>> {
614        Ok(None)
615    }
616
617    async fn table_info_by_id(&self, _table_id: TableId) -> CatalogResult<Option<TableInfoRef>> {
618        Ok(None)
619    }
620
621    async fn tables_by_ids(
622        &self,
623        _catalog: &str,
624        _schema: &str,
625        _table_ids: &[TableId],
626    ) -> CatalogResult<Vec<TableRef>> {
627        Ok(vec![])
628    }
629
630    fn tables<'a>(
631        &'a self,
632        _catalog: &'a str,
633        _schema: &'a str,
634        _query_ctx: Option<&'a QueryContext>,
635    ) -> BoxStream<'a, CatalogResult<TableRef>> {
636        Box::pin(futures::stream::empty())
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use std::collections::HashMap;
643    use std::sync::{Arc, RwLock};
644
645    use common_error::ext::ErrorExt;
646    use common_error::status_code::StatusCode;
647    use session::context::QueryContextBuilder;
648
649    use super::*;
650    use crate::error::Error;
651    use crate::options::{
652        FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE,
653        FLOW_RETURN_REGION_SEQ, FLOW_SINK_TABLE_ID,
654    };
655
656    #[test]
657    fn test_nullable_label_filter_pushdown() {
658        use datafusion_expr::{Cast, col, lit};
659        use datatypes::arrow::datatypes::DataType as ArrowDataType;
660
661        let provider = crate::optimizer::test_util::mock_table_provider(RegionId::new(1, 1));
662        let cast_label = Expr::Cast(Cast::new(Box::new(col("k0")), ArrowDataType::Utf8));
663        let cast_field = Expr::Cast(Cast::new(Box::new(col("v0")), ArrowDataType::Utf8));
664        let filters = [
665            col("k0").is_null().or(col("k0").not_eq(lit("lo"))),
666            col("k0").is_null().or(cast_label.not_eq(lit("lo"))),
667            col("k0").is_null().or(col("v0").not_eq(lit(1.0))),
668            cast_field.eq(lit("1")),
669            col("k0").in_list(vec![lit(""), lit("lo")], false),
670            col("k0").in_list(vec![lit(""), lit("lo")], true),
671        ];
672        assert_eq!(
673            provider
674                .supports_filters_pushdown(&filters.iter().collect::<Vec<_>>())
675                .unwrap(),
676            vec![
677                TableProviderFilterPushDown::Exact,
678                TableProviderFilterPushDown::Exact,
679                TableProviderFilterPushDown::Inexact,
680                TableProviderFilterPushDown::Inexact,
681                TableProviderFilterPushDown::Inexact,
682                TableProviderFilterPushDown::Inexact,
683            ]
684        );
685    }
686
687    fn test_region_id() -> RegionId {
688        RegionId::new(1024, 1)
689    }
690
691    #[test]
692    fn test_scan_request_from_query_context_uses_snapshot_bound_intent() {
693        let region_id = test_region_id();
694        let query_ctx = QueryContextBuilder::default()
695            .extensions(HashMap::from([(
696                "flow.return_region_seq".to_string(),
697                "true".to_string(),
698            )]))
699            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
700                region_id.as_u64(),
701                42_u64,
702            )]))))
703            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
704                region_id.as_u64(),
705                7_u64,
706            )]))))
707            .build();
708
709        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
710
711        assert!(!request.snapshot_on_scan);
712        assert_eq!(request.memtable_max_sequence, Some(42));
713        assert_eq!(request.sst_min_sequence, Some(7));
714    }
715
716    #[test]
717    fn test_terminal_watermark_context_source_and_sink_scan_semantics() {
718        let region_id = test_region_id();
719        let query_ctx = QueryContextBuilder::default()
720            .extensions(HashMap::from([(
721                FLOW_RETURN_REGION_SEQ.to_string(),
722                "true".to_string(),
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, None);
730        assert_eq!(request.memtable_max_sequence, None);
731        assert_eq!(request.sst_min_sequence, None);
732
733        let query_ctx = QueryContextBuilder::default()
734            .extensions(HashMap::from([
735                (FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string()),
736                (
737                    FLOW_SINK_TABLE_ID.to_string(),
738                    region_id.table_id().to_string(),
739                ),
740            ]))
741            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
742                region_id.as_u64(),
743                88_u64,
744            )]))))
745            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
746                region_id.as_u64(),
747                77_u64,
748            )]))))
749            .build();
750
751        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
752
753        assert!(!request.snapshot_on_scan);
754        assert_eq!(request.memtable_min_sequence, None);
755        assert_eq!(request.memtable_max_sequence, None);
756        assert_eq!(request.sst_min_sequence, None);
757    }
758
759    #[test]
760    fn test_scan_request_from_incremental_context_uses_snapshot_bound_intent() {
761        let region_id = test_region_id();
762        let query_ctx = QueryContextBuilder::default()
763            .extensions(HashMap::from([(
764                "flow.incremental_after_seqs".to_string(),
765                format!(r#"{{"{}":10}}"#, region_id.as_u64()),
766            )]))
767            .build();
768
769        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
770
771        assert!(request.snapshot_on_scan);
772        assert_eq!(request.memtable_min_sequence, Some(10));
773        assert_eq!(request.memtable_max_sequence, None);
774    }
775
776    #[test]
777    fn test_scan_request_from_sequence_range_context_uses_exact_source_scan() {
778        let region_id = test_region_id();
779        let query_ctx = QueryContextBuilder::default()
780            .extensions(HashMap::from([
781                (
782                    FLOW_INCREMENTAL_MODE.to_string(),
783                    FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
784                ),
785                (
786                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
787                    format!(r#"{{"{}":10}}"#, region_id.as_u64()),
788                ),
789            ]))
790            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
791                region_id.as_u64(),
792                42_u64,
793            )]))))
794            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
795                region_id.as_u64(),
796                7_u64,
797            )]))))
798            .build();
799
800        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
801
802        assert!(request.exact_sequence_range);
803        assert!(!request.skip_sst_files);
804        assert_eq!(request.memtable_min_sequence, Some(10));
805        assert_eq!(request.memtable_max_sequence, Some(42));
806        assert_eq!(request.sst_min_sequence, None);
807    }
808
809    #[test]
810    fn test_scan_request_from_sequence_range_context_binds_snapshot_on_scan() {
811        let region_id = test_region_id();
812        let query_ctx = QueryContextBuilder::default()
813            .extensions(HashMap::from([
814                (
815                    FLOW_INCREMENTAL_MODE.to_string(),
816                    FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
817                ),
818                (
819                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
820                    format!(r#"{{"{}":10}}"#, region_id.as_u64()),
821                ),
822            ]))
823            .build();
824
825        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
826
827        assert!(request.exact_sequence_range);
828        assert!(request.snapshot_on_scan);
829        assert_eq!(request.memtable_min_sequence, Some(10));
830        assert_eq!(request.memtable_max_sequence, None);
831        assert!(!request.skip_sst_files);
832    }
833
834    #[test]
835    fn test_scan_request_from_query_context_keeps_snapshot_fields() {
836        let region_id = test_region_id();
837        let query_ctx = QueryContextBuilder::default()
838            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
839                region_id.as_u64(),
840                100,
841            )]))))
842            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
843                region_id.as_u64(),
844                90,
845            )]))))
846            .build();
847
848        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
849        assert_eq!(request.memtable_max_sequence, Some(100));
850        assert_eq!(request.sst_min_sequence, Some(90));
851        assert_eq!(request.memtable_min_sequence, None);
852        assert!(!request.snapshot_on_scan);
853        assert!(!request.skip_sst_files);
854    }
855
856    #[test]
857    fn test_scan_request_from_query_context_reuses_existing_snapshot_for_incremental_scan() {
858        let region_id = test_region_id();
859        let query_ctx = QueryContextBuilder::default()
860            .extensions(HashMap::from([
861                (
862                    FLOW_INCREMENTAL_MODE.to_string(),
863                    "memtable_only".to_string(),
864                ),
865                (
866                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
867                    format!(r#"{{"{}":10}}"#, region_id.as_u64()),
868                ),
869            ]))
870            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
871                region_id.as_u64(),
872                42_u64,
873            )]))))
874            .build();
875
876        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
877
878        assert_eq!(request.memtable_min_sequence, Some(10));
879        assert_eq!(request.memtable_max_sequence, Some(42));
880        assert!(!request.snapshot_on_scan);
881        assert!(request.skip_sst_files);
882    }
883
884    #[test]
885    fn test_apply_cached_snapshot_to_request_updates_cached_scan_request() {
886        let region_id = test_region_id();
887        let query_ctx = QueryContextBuilder::default()
888            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
889                region_id.as_u64(),
890                88_u64,
891            )]))))
892            .build();
893        let mut request = ScanRequest {
894            snapshot_on_scan: true,
895            ..Default::default()
896        };
897
898        apply_cached_snapshot_to_request(&query_ctx, region_id, false, &mut request);
899
900        assert_eq!(request.memtable_max_sequence, Some(88));
901        assert!(!request.snapshot_on_scan);
902    }
903
904    #[test]
905    fn test_apply_cached_snapshot_to_request_preserves_exact_sequence_range() {
906        let region_id = test_region_id();
907        let query_ctx = QueryContextBuilder::default()
908            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
909                region_id.as_u64(),
910                10_u64,
911            )]))))
912            .build();
913        let mut request = ScanRequest {
914            memtable_min_sequence: Some(10),
915            snapshot_on_scan: true,
916            exact_sequence_range: true,
917            ..Default::default()
918        };
919
920        apply_cached_snapshot_to_request(&query_ctx, region_id, false, &mut request);
921
922        assert_eq!(request.memtable_min_sequence, Some(10));
923        assert_eq!(request.memtable_max_sequence, Some(10));
924        assert!(request.exact_sequence_range);
925        assert!(!request.skip_sst_files);
926        assert_eq!(request.sst_min_sequence, None);
927        assert!(!request.snapshot_on_scan);
928    }
929
930    #[test]
931    fn test_apply_cached_snapshot_to_request_skips_sink_scan() {
932        let region_id = test_region_id();
933        let query_ctx = QueryContextBuilder::default()
934            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
935                region_id.as_u64(),
936                88_u64,
937            )]))))
938            .build();
939        let mut request = ScanRequest {
940            snapshot_on_scan: true,
941            ..Default::default()
942        };
943
944        apply_cached_snapshot_to_request(&query_ctx, region_id, true, &mut request);
945
946        assert_eq!(request.memtable_max_sequence, None);
947        assert!(request.snapshot_on_scan);
948    }
949
950    #[test]
951    fn test_bind_snapshot_bound_region_seq_reuses_existing_snapshot() {
952        let region_id = test_region_id();
953        let query_ctx = QueryContextBuilder::default()
954            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
955                region_id.as_u64(),
956                42_u64,
957            )]))))
958            .build();
959
960        let err = bind_snapshot_bound_region_seq(&query_ctx, region_id, 99).unwrap_err();
961
962        assert!(matches!(err, Error::ConflictingSnapshotSequence { .. }));
963        assert_eq!(query_ctx.get_snapshot(region_id.as_u64()), Some(42));
964    }
965
966    #[test]
967    fn test_bind_snapshot_bound_region_seq_sets_snapshot_once() {
968        let region_id = test_region_id();
969        let query_ctx = QueryContextBuilder::default().build();
970
971        let seq = bind_snapshot_bound_region_seq(&query_ctx, region_id, 99).unwrap();
972
973        assert_eq!(seq, 99);
974        assert_eq!(query_ctx.get_snapshot(region_id.as_u64()), Some(99));
975    }
976
977    #[test]
978    fn test_scan_request_from_query_context_applies_incremental_after_seq_for_source_scan() {
979        let region_id = test_region_id();
980        let query_ctx = QueryContextBuilder::default()
981            .extensions(HashMap::from([
982                (
983                    FLOW_INCREMENTAL_MODE.to_string(),
984                    "memtable_only".to_string(),
985                ),
986                (
987                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
988                    format!(r#"{{"{}":55}}"#, region_id.as_u64()),
989                ),
990            ]))
991            .build();
992
993        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
994        assert_eq!(request.memtable_min_sequence, Some(55));
995        assert_eq!(request.sst_min_sequence, None);
996        assert!(request.skip_sst_files);
997        assert!(!request.exact_sequence_range);
998    }
999
1000    #[test]
1001    fn test_scan_request_from_sequence_range_context_excludes_sink_scan() {
1002        let region_id = test_region_id();
1003        let query_ctx = QueryContextBuilder::default()
1004            .extensions(HashMap::from([
1005                (
1006                    FLOW_INCREMENTAL_MODE.to_string(),
1007                    FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
1008                ),
1009                (
1010                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
1011                    format!(r#"{{"{}":55}}"#, region_id.as_u64()),
1012                ),
1013                (
1014                    FLOW_SINK_TABLE_ID.to_string(),
1015                    region_id.table_id().to_string(),
1016                ),
1017            ]))
1018            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
1019                region_id.as_u64(),
1020                88_u64,
1021            )]))))
1022            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
1023                region_id.as_u64(),
1024                77_u64,
1025            )]))))
1026            .build();
1027
1028        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
1029
1030        assert!(!request.exact_sequence_range);
1031        assert!(!request.skip_sst_files);
1032        assert_eq!(request.memtable_min_sequence, None);
1033        assert_eq!(request.memtable_max_sequence, None);
1034        assert_eq!(request.sst_min_sequence, None);
1035    }
1036
1037    #[test]
1038    fn test_scan_request_from_query_context_does_not_apply_incremental_for_sink_table() {
1039        let region_id = test_region_id();
1040        let query_ctx = QueryContextBuilder::default()
1041            .extensions(HashMap::from([
1042                (
1043                    FLOW_INCREMENTAL_MODE.to_string(),
1044                    "memtable_only".to_string(),
1045                ),
1046                (
1047                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
1048                    format!(r#"{{"{}":55}}"#, region_id.as_u64()),
1049                ),
1050                (
1051                    FLOW_SINK_TABLE_ID.to_string(),
1052                    region_id.table_id().to_string(),
1053                ),
1054            ]))
1055            .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
1056                region_id.as_u64(),
1057                88_u64,
1058            )]))))
1059            .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
1060                region_id.as_u64(),
1061                77_u64,
1062            )]))))
1063            .build();
1064
1065        let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
1066        assert_eq!(request.memtable_min_sequence, None);
1067        assert_eq!(request.memtable_max_sequence, None);
1068        assert_eq!(request.sst_min_sequence, None);
1069        assert!(!request.skip_sst_files);
1070        assert!(!request.snapshot_on_scan);
1071    }
1072
1073    #[test]
1074    fn test_scan_request_from_query_context_rejects_missing_memtable_only_region() {
1075        let region_id = test_region_id();
1076        let query_ctx = QueryContextBuilder::default()
1077            .extensions(HashMap::from([
1078                (
1079                    FLOW_INCREMENTAL_MODE.to_string(),
1080                    "memtable_only".to_string(),
1081                ),
1082                (
1083                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
1084                    r#"{"9":55}"#.to_string(),
1085                ),
1086            ]))
1087            .build();
1088
1089        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
1090        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
1091    }
1092
1093    #[test]
1094    fn test_scan_request_from_sequence_range_rejects_missing_source_bound() {
1095        let region_id = test_region_id();
1096        let query_ctx = QueryContextBuilder::default()
1097            .extensions(HashMap::from([
1098                (
1099                    FLOW_INCREMENTAL_MODE.to_string(),
1100                    FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
1101                ),
1102                (
1103                    FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
1104                    r#"{"9":55}"#.to_string(),
1105                ),
1106            ]))
1107            .build();
1108
1109        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
1110        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
1111        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
1112    }
1113
1114    #[test]
1115    fn test_scan_request_from_query_context_rejects_invalid_incremental_json() {
1116        let region_id = test_region_id();
1117        let query_ctx = QueryContextBuilder::default()
1118            .extensions(HashMap::from([(
1119                FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
1120                "not-json".to_string(),
1121            )]))
1122            .build();
1123
1124        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
1125        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
1126        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
1127    }
1128
1129    #[test]
1130    fn test_scan_request_from_query_context_rejects_invalid_sink_table_id() {
1131        let region_id = test_region_id();
1132        let query_ctx = QueryContextBuilder::default()
1133            .extensions(HashMap::from([(
1134                FLOW_SINK_TABLE_ID.to_string(),
1135                "abc".to_string(),
1136            )]))
1137            .build();
1138
1139        let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
1140        assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
1141        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
1142    }
1143}