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::{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#[derive(Clone, Debug)]
51pub struct DummyCatalogList {
52 catalog: DummyCatalogProvider,
53}
54
55impl DummyCatalogList {
56 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#[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#[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#[derive(Clone)]
130pub struct DummyTableProvider {
131 region_id: RegionId,
132 engine: RegionEngineRef,
133 metadata: RegionMetadataRef,
134 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 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 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 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 pub fn with_distribution(&self, distribution: TimeSeriesDistribution) {
263 self.scan_request.lock().unwrap().distribution = Some(distribution);
264 }
265
266 pub fn with_time_series_selector_hint(&self, selector: TimeSeriesRowSelector) {
268 self.scan_request.lock().unwrap().series_row_selector = Some(selector);
269 }
270
271 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 #[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 is_sink_scan: bool,
346 snapshot_on_scan: bool,
350 memtable_min_sequence: Option<u64>,
353 memtable_max_sequence: Option<u64>,
357 skip_sst_files: bool,
359 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 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(®ion_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 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 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 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
528pub struct DummyCatalogManager;
532
533impl DummyCatalogManager {
534 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}