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