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