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::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#[derive(Clone, Debug)]
53pub struct DummyCatalogList {
54 catalog: DummyCatalogProvider,
55}
56
57impl DummyCatalogList {
58 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#[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#[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#[derive(Clone)]
144pub struct DummyTableProvider {
145 region_id: RegionId,
146 engine: RegionEngineRef,
147 metadata: RegionMetadataRef,
148 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 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 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 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 pub fn with_distribution(&self, distribution: TimeSeriesDistribution) {
281 self.scan_request.lock().unwrap().distribution = Some(distribution);
282 }
283
284 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 #[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 is_sink_scan: bool,
361 snapshot_on_scan: bool,
365 memtable_min_sequence: Option<u64>,
368 memtable_max_sequence: Option<u64>,
372 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 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(®ion_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 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 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 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
536pub struct DummyCatalogManager;
540
541impl DummyCatalogManager {
542 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}