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