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