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
188 if let Some(query_ctx) = &self.query_ctx {
189 let is_sink_scan = is_sink_scan(query_ctx, self.region_id)
190 .map_err(|e| DataFusionError::External(Box::new(e)))?;
191 apply_cached_snapshot_to_request(query_ctx, self.region_id, is_sink_scan, &mut request);
192 }
193
194 let scanner = self
195 .engine
196 .handle_query(self.region_id, request.clone())
197 .await
198 .map_err(|e| DataFusionError::External(Box::new(e)))?;
199
200 if request.snapshot_on_scan
201 && let Some(query_ctx) = &self.query_ctx
202 && let Some(snapshot_sequence) = scanner.snapshot_sequence()
203 {
204 bind_snapshot_bound_region_seq(query_ctx, self.region_id, snapshot_sequence)
205 .map_err(|e| DataFusionError::External(Box::new(e)))?;
206 }
207
208 let query_memory_tracker = self.engine.query_memory_tracker();
209 let mut scan_exec = RegionScanExec::new(scanner, request, query_memory_tracker)?;
210 if let Some(query_ctx) = &self.query_ctx {
211 scan_exec.set_explain_verbose(query_ctx.explain_verbose());
212 }
213 Ok(Arc::new(scan_exec))
214 }
215
216 fn supports_filters_pushdown(
217 &self,
218 filters: &[&Expr],
219 ) -> datafusion::error::Result<Vec<TableProviderFilterPushDown>> {
220 let supported = filters
221 .iter()
222 .map(|e| {
223 if let Some(simple_filter) = SimpleFilterEvaluator::try_new(e) {
225 if self
226 .metadata
227 .column_by_name(simple_filter.column_name())
228 .and_then(|c| {
229 (c.semantic_type == SemanticType::Tag
230 || c.semantic_type == SemanticType::Timestamp)
231 .then_some(())
232 })
233 .is_some()
234 {
235 TableProviderFilterPushDown::Exact
236 } else {
237 TableProviderFilterPushDown::Inexact
238 }
239 } else {
240 TableProviderFilterPushDown::Inexact
241 }
242 })
243 .collect();
244 Ok(supported)
245 }
246}
247
248impl DummyTableProvider {
249 pub fn new(region_id: RegionId, engine: RegionEngineRef, metadata: RegionMetadataRef) -> Self {
251 Self {
252 region_id,
253 engine,
254 metadata,
255 scan_request: Default::default(),
256 query_ctx: None,
257 }
258 }
259
260 pub fn region_metadata(&self) -> RegionMetadataRef {
261 self.metadata.clone()
262 }
263
264 pub fn with_ordering_hint(&self, order_opts: &[OrderOption]) {
266 self.scan_request.lock().unwrap().output_ordering = Some(order_opts.to_vec());
267 }
268
269 pub fn with_distribution(&self, distribution: TimeSeriesDistribution) {
271 self.scan_request.lock().unwrap().distribution = Some(distribution);
272 }
273
274 pub fn with_time_series_selector_hint(&self, selector: TimeSeriesRowSelector) {
276 self.scan_request.lock().unwrap().series_row_selector = Some(selector);
277 }
278
279 pub fn with_vector_search_hint(&self, hint: VectorSearchRequest) {
280 self.scan_request.lock().unwrap().vector_search = Some(hint);
281 }
282
283 pub fn get_vector_search_hint(&self) -> Option<VectorSearchRequest> {
284 self.scan_request.lock().unwrap().vector_search.clone()
285 }
286
287 pub fn with_sequence(&self, sequence: u64) {
288 self.scan_request.lock().unwrap().memtable_max_sequence = Some(sequence);
289 }
290
291 pub(crate) fn with_json_type_hint(&self, hint: HashMap<String, JsonNativeType>) {
292 self.scan_request.lock().unwrap().json_type_hint = hint;
293 }
294
295 #[cfg(test)]
297 pub fn scan_request(&self) -> ScanRequest {
298 self.scan_request.lock().unwrap().clone()
299 }
300}
301
302pub struct DummyTableProviderFactory;
303
304impl DummyTableProviderFactory {
305 pub async fn create_table_provider(
306 &self,
307 region_id: RegionId,
308 engine: RegionEngineRef,
309 query_ctx: Option<QueryContextRef>,
310 ) -> Result<DummyTableProvider> {
311 let metadata =
312 engine
313 .get_metadata(region_id)
314 .await
315 .with_context(|_| GetRegionMetadataSnafu {
316 engine: engine.name(),
317 region_id,
318 })?;
319
320 let scan_request = if let Some(ctx) = query_ctx.as_ref() {
321 scan_request_from_query_context(region_id, ctx)?
322 } else {
323 ScanRequest::default()
324 };
325
326 Ok(DummyTableProvider {
327 region_id,
328 engine,
329 metadata,
330 scan_request: Arc::new(Mutex::new(scan_request)),
331 query_ctx,
332 })
333 }
334}
335
336fn scan_request_from_query_context(
337 region_id: RegionId,
338 query_ctx: &QueryContext,
339) -> Result<ScanRequest> {
340 let decision = decide_flow_scan(query_ctx, region_id)?;
341 Ok(build_scan_request(query_ctx, region_id, &decision))
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
345struct FlowScanDecision {
346 is_sink_scan: bool,
349 snapshot_on_scan: bool,
353 memtable_min_sequence: Option<u64>,
356 memtable_max_sequence: Option<u64>,
360 skip_sst_files: bool,
362}
363
364impl FlowScanDecision {
365 fn plain_scan() -> Self {
366 Self {
367 is_sink_scan: true,
368 snapshot_on_scan: false,
369 memtable_min_sequence: None,
370 memtable_max_sequence: None,
371 skip_sst_files: 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 });
387 };
388
389 if flow_extensions.sink_table_id == Some(region_id.table_id()) {
393 return Ok(FlowScanDecision::plain_scan());
394 }
395
396 let apply_incremental = flow_extensions.validate_for_scan(region_id)?;
397
398 let memtable_min_sequence = if apply_incremental {
399 flow_extensions
400 .incremental_after_seqs
401 .as_ref()
402 .and_then(|seqs| seqs.get(®ion_id.as_u64()))
403 .copied()
404 } else {
405 None
406 };
407
408 let memtable_max_sequence = query_ctx.get_snapshot(region_id.as_u64());
409
410 let skip_sst_files = apply_incremental
411 && flow_extensions.incremental_mode == Some(FlowIncrementalMode::MemtableOnly);
412
413 Ok(FlowScanDecision {
414 is_sink_scan: false,
415 snapshot_on_scan: memtable_max_sequence.is_none()
416 && flow_extensions.should_collect_region_watermark(),
417 memtable_min_sequence,
418 memtable_max_sequence,
419 skip_sst_files,
420 })
421}
422
423fn build_scan_request(
424 query_ctx: &QueryContext,
425 region_id: RegionId,
426 decision: &FlowScanDecision,
427) -> ScanRequest {
428 ScanRequest {
432 sst_min_sequence: (!decision.is_sink_scan)
433 .then(|| query_ctx.sst_min_sequence(region_id.as_u64()))
434 .flatten(),
435 skip_sst_files: decision.skip_sst_files,
436 snapshot_on_scan: decision.snapshot_on_scan,
437 memtable_min_sequence: decision.memtable_min_sequence,
438 memtable_max_sequence: decision.memtable_max_sequence,
439 ..Default::default()
440 }
441}
442
443fn is_sink_scan(query_ctx: &QueryContext, region_id: RegionId) -> Result<bool> {
444 Ok(
445 FlowQueryExtensions::parse_flow_extensions(&query_ctx.extensions())?
446 .is_some_and(|exts| exts.sink_table_id == Some(region_id.table_id())),
447 )
448}
449
450fn apply_cached_snapshot_to_request(
451 query_ctx: &QueryContext,
452 region_id: RegionId,
453 is_sink_scan: bool,
454 scan_request: &mut ScanRequest,
455) {
456 if is_sink_scan {
457 return;
458 }
459
460 if let Some(snapshot_sequence) = query_ctx.get_snapshot(region_id.as_u64()) {
461 scan_request.memtable_max_sequence = Some(snapshot_sequence);
466 scan_request.snapshot_on_scan = false;
467 }
468}
469
470fn bind_snapshot_bound_region_seq(
471 query_ctx: &QueryContext,
472 region_id: RegionId,
473 snapshot_sequence: u64,
474) -> Result<u64> {
475 if let Some(existing) = query_ctx.get_snapshot(region_id.as_u64()) {
476 if existing != snapshot_sequence {
477 return crate::error::ConflictingSnapshotSequenceSnafu {
478 region_id,
479 existing,
480 new: snapshot_sequence,
481 }
482 .fail();
483 }
484 Ok(existing)
485 } else {
486 query_ctx.set_snapshot(region_id.as_u64(), snapshot_sequence);
487 Ok(snapshot_sequence)
488 }
489}
490
491#[async_trait]
492impl TableProviderFactory for DummyTableProviderFactory {
493 async fn create(
494 &self,
495 region_id: RegionId,
496 engine: RegionEngineRef,
497 ctx: Option<QueryContextRef>,
498 ) -> Result<Arc<dyn TableProvider>> {
499 let provider = self.create_table_provider(region_id, engine, ctx).await?;
500 Ok(Arc::new(provider))
501 }
502}
503
504#[async_trait]
505pub trait TableProviderFactory: Send + Sync {
506 async fn create(
507 &self,
508 region_id: RegionId,
509 engine: RegionEngineRef,
510 ctx: Option<QueryContextRef>,
511 ) -> Result<Arc<dyn TableProvider>>;
512}
513
514pub type TableProviderFactoryRef = Arc<dyn TableProviderFactory>;
515
516pub struct DummyCatalogManager;
520
521impl DummyCatalogManager {
522 pub fn arc() -> CatalogManagerRef {
524 Arc::new(Self)
525 }
526}
527
528#[async_trait::async_trait]
529impl CatalogManager for DummyCatalogManager {
530 fn as_any(&self) -> &dyn Any {
531 self
532 }
533
534 async fn catalog_names(&self) -> CatalogResult<Vec<String>> {
535 Ok(vec![])
536 }
537
538 async fn schema_names(
539 &self,
540 _catalog: &str,
541 _query_ctx: Option<&QueryContext>,
542 ) -> CatalogResult<Vec<String>> {
543 Ok(vec![])
544 }
545
546 async fn table_names(
547 &self,
548 _catalog: &str,
549 _schema: &str,
550 _query_ctx: Option<&QueryContext>,
551 ) -> CatalogResult<Vec<String>> {
552 Ok(vec![])
553 }
554
555 async fn catalog_exists(&self, _catalog: &str) -> CatalogResult<bool> {
556 Ok(false)
557 }
558
559 async fn schema_exists(
560 &self,
561 _catalog: &str,
562 _schema: &str,
563 _query_ctx: Option<&QueryContext>,
564 ) -> CatalogResult<bool> {
565 Ok(false)
566 }
567
568 async fn table_exists(
569 &self,
570 _catalog: &str,
571 _schema: &str,
572 _table: &str,
573 _query_ctx: Option<&QueryContext>,
574 ) -> CatalogResult<bool> {
575 Ok(false)
576 }
577
578 async fn table(
579 &self,
580 _catalog: &str,
581 _schema: &str,
582 _table_name: &str,
583 _query_ctx: Option<&QueryContext>,
584 ) -> CatalogResult<Option<TableRef>> {
585 Ok(None)
586 }
587
588 async fn table_id(
589 &self,
590 _catalog: &str,
591 _schema: &str,
592 _table_name: &str,
593 _query_ctx: Option<&QueryContext>,
594 ) -> CatalogResult<Option<TableId>> {
595 Ok(None)
596 }
597
598 async fn table_info_by_id(&self, _table_id: TableId) -> CatalogResult<Option<TableInfoRef>> {
599 Ok(None)
600 }
601
602 async fn tables_by_ids(
603 &self,
604 _catalog: &str,
605 _schema: &str,
606 _table_ids: &[TableId],
607 ) -> CatalogResult<Vec<TableRef>> {
608 Ok(vec![])
609 }
610
611 fn tables<'a>(
612 &'a self,
613 _catalog: &'a str,
614 _schema: &'a str,
615 _query_ctx: Option<&'a QueryContext>,
616 ) -> BoxStream<'a, CatalogResult<TableRef>> {
617 Box::pin(futures::stream::empty())
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use std::collections::HashMap;
624 use std::sync::{Arc, RwLock};
625
626 use common_error::ext::ErrorExt;
627 use common_error::status_code::StatusCode;
628 use session::context::QueryContextBuilder;
629
630 use super::*;
631 use crate::error::Error;
632 use crate::options::{
633 FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_RETURN_REGION_SEQ,
634 FLOW_SINK_TABLE_ID,
635 };
636
637 fn test_region_id() -> RegionId {
638 RegionId::new(1024, 1)
639 }
640
641 #[test]
642 fn test_scan_request_from_query_context_uses_snapshot_bound_intent() {
643 let region_id = test_region_id();
644 let query_ctx = QueryContextBuilder::default()
645 .extensions(HashMap::from([(
646 "flow.return_region_seq".to_string(),
647 "true".to_string(),
648 )]))
649 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
650 region_id.as_u64(),
651 42_u64,
652 )]))))
653 .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
654 region_id.as_u64(),
655 7_u64,
656 )]))))
657 .build();
658
659 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
660
661 assert!(!request.snapshot_on_scan);
662 assert_eq!(request.memtable_max_sequence, Some(42));
663 assert_eq!(request.sst_min_sequence, Some(7));
664 }
665
666 #[test]
667 fn test_terminal_watermark_context_source_and_sink_scan_semantics() {
668 let region_id = test_region_id();
669 let query_ctx = QueryContextBuilder::default()
670 .extensions(HashMap::from([(
671 FLOW_RETURN_REGION_SEQ.to_string(),
672 "true".to_string(),
673 )]))
674 .build();
675
676 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
677
678 assert!(request.snapshot_on_scan);
679 assert_eq!(request.memtable_min_sequence, None);
680 assert_eq!(request.memtable_max_sequence, None);
681 assert_eq!(request.sst_min_sequence, None);
682
683 let query_ctx = QueryContextBuilder::default()
684 .extensions(HashMap::from([
685 (FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string()),
686 (
687 FLOW_SINK_TABLE_ID.to_string(),
688 region_id.table_id().to_string(),
689 ),
690 ]))
691 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
692 region_id.as_u64(),
693 88_u64,
694 )]))))
695 .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
696 region_id.as_u64(),
697 77_u64,
698 )]))))
699 .build();
700
701 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
702
703 assert!(!request.snapshot_on_scan);
704 assert_eq!(request.memtable_min_sequence, None);
705 assert_eq!(request.memtable_max_sequence, None);
706 assert_eq!(request.sst_min_sequence, None);
707 }
708
709 #[test]
710 fn test_scan_request_from_incremental_context_uses_snapshot_bound_intent() {
711 let region_id = test_region_id();
712 let query_ctx = QueryContextBuilder::default()
713 .extensions(HashMap::from([(
714 "flow.incremental_after_seqs".to_string(),
715 format!(r#"{{"{}":10}}"#, region_id.as_u64()),
716 )]))
717 .build();
718
719 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
720
721 assert!(request.snapshot_on_scan);
722 assert_eq!(request.memtable_min_sequence, Some(10));
723 assert_eq!(request.memtable_max_sequence, None);
724 }
725
726 #[test]
727 fn test_scan_request_from_query_context_keeps_snapshot_fields() {
728 let region_id = test_region_id();
729 let query_ctx = QueryContextBuilder::default()
730 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
731 region_id.as_u64(),
732 100,
733 )]))))
734 .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
735 region_id.as_u64(),
736 90,
737 )]))))
738 .build();
739
740 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
741 assert_eq!(request.memtable_max_sequence, Some(100));
742 assert_eq!(request.sst_min_sequence, Some(90));
743 assert_eq!(request.memtable_min_sequence, None);
744 assert!(!request.snapshot_on_scan);
745 }
746
747 #[test]
748 fn test_scan_request_from_query_context_reuses_existing_snapshot_for_incremental_scan() {
749 let region_id = test_region_id();
750 let query_ctx = QueryContextBuilder::default()
751 .extensions(HashMap::from([(
752 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
753 format!(r#"{{"{}":10}}"#, region_id.as_u64()),
754 )]))
755 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
756 region_id.as_u64(),
757 42_u64,
758 )]))))
759 .build();
760
761 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
762
763 assert_eq!(request.memtable_min_sequence, Some(10));
764 assert_eq!(request.memtable_max_sequence, Some(42));
765 assert!(!request.snapshot_on_scan);
766 }
767
768 #[test]
769 fn test_apply_cached_snapshot_to_request_updates_cached_scan_request() {
770 let region_id = test_region_id();
771 let query_ctx = QueryContextBuilder::default()
772 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
773 region_id.as_u64(),
774 88_u64,
775 )]))))
776 .build();
777 let mut request = ScanRequest {
778 snapshot_on_scan: true,
779 ..Default::default()
780 };
781
782 apply_cached_snapshot_to_request(&query_ctx, region_id, false, &mut request);
783
784 assert_eq!(request.memtable_max_sequence, Some(88));
785 assert!(!request.snapshot_on_scan);
786 }
787
788 #[test]
789 fn test_apply_cached_snapshot_to_request_skips_sink_scan() {
790 let region_id = test_region_id();
791 let query_ctx = QueryContextBuilder::default()
792 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
793 region_id.as_u64(),
794 88_u64,
795 )]))))
796 .build();
797 let mut request = ScanRequest {
798 snapshot_on_scan: true,
799 ..Default::default()
800 };
801
802 apply_cached_snapshot_to_request(&query_ctx, region_id, true, &mut request);
803
804 assert_eq!(request.memtable_max_sequence, None);
805 assert!(request.snapshot_on_scan);
806 }
807
808 #[test]
809 fn test_bind_snapshot_bound_region_seq_reuses_existing_snapshot() {
810 let region_id = test_region_id();
811 let query_ctx = QueryContextBuilder::default()
812 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
813 region_id.as_u64(),
814 42_u64,
815 )]))))
816 .build();
817
818 let err = bind_snapshot_bound_region_seq(&query_ctx, region_id, 99).unwrap_err();
819
820 assert!(matches!(err, Error::ConflictingSnapshotSequence { .. }));
821 assert_eq!(query_ctx.get_snapshot(region_id.as_u64()), Some(42));
822 }
823
824 #[test]
825 fn test_bind_snapshot_bound_region_seq_sets_snapshot_once() {
826 let region_id = test_region_id();
827 let query_ctx = QueryContextBuilder::default().build();
828
829 let seq = bind_snapshot_bound_region_seq(&query_ctx, region_id, 99).unwrap();
830
831 assert_eq!(seq, 99);
832 assert_eq!(query_ctx.get_snapshot(region_id.as_u64()), Some(99));
833 }
834
835 #[test]
836 fn test_scan_request_from_query_context_applies_incremental_after_seq_for_source_scan() {
837 let region_id = test_region_id();
838 let query_ctx = QueryContextBuilder::default()
839 .extensions(HashMap::from([
840 (
841 FLOW_INCREMENTAL_MODE.to_string(),
842 "memtable_only".to_string(),
843 ),
844 (
845 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
846 format!(r#"{{"{}":55}}"#, region_id.as_u64()),
847 ),
848 ]))
849 .build();
850
851 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
852 assert_eq!(request.memtable_min_sequence, Some(55));
853 assert_eq!(request.sst_min_sequence, None);
854 assert!(request.skip_sst_files);
855 }
856
857 #[test]
858 fn test_scan_request_from_query_context_does_not_apply_incremental_for_sink_table() {
859 let region_id = test_region_id();
860 let query_ctx = QueryContextBuilder::default()
861 .extensions(HashMap::from([
862 (
863 FLOW_INCREMENTAL_MODE.to_string(),
864 "memtable_only".to_string(),
865 ),
866 (
867 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
868 format!(r#"{{"{}":55}}"#, region_id.as_u64()),
869 ),
870 (
871 FLOW_SINK_TABLE_ID.to_string(),
872 region_id.table_id().to_string(),
873 ),
874 ]))
875 .snapshot_seqs(Arc::new(RwLock::new(HashMap::from([(
876 region_id.as_u64(),
877 88_u64,
878 )]))))
879 .sst_min_sequences(Arc::new(RwLock::new(HashMap::from([(
880 region_id.as_u64(),
881 77_u64,
882 )]))))
883 .build();
884
885 let request = scan_request_from_query_context(region_id, &query_ctx).unwrap();
886 assert_eq!(request.memtable_min_sequence, None);
887 assert_eq!(request.memtable_max_sequence, None);
888 assert_eq!(request.sst_min_sequence, None);
889 assert!(!request.skip_sst_files);
890 assert!(!request.snapshot_on_scan);
891 }
892
893 #[test]
894 fn test_scan_request_from_query_context_rejects_missing_memtable_only_region() {
895 let region_id = test_region_id();
896 let query_ctx = QueryContextBuilder::default()
897 .extensions(HashMap::from([
898 (
899 FLOW_INCREMENTAL_MODE.to_string(),
900 "memtable_only".to_string(),
901 ),
902 (
903 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
904 r#"{"9":55}"#.to_string(),
905 ),
906 ]))
907 .build();
908
909 let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
910 assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
911 }
912
913 #[test]
914 fn test_scan_request_from_query_context_rejects_invalid_incremental_json() {
915 let region_id = test_region_id();
916 let query_ctx = QueryContextBuilder::default()
917 .extensions(HashMap::from([(
918 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
919 "not-json".to_string(),
920 )]))
921 .build();
922
923 let err = scan_request_from_query_context(region_id, &query_ctx).unwrap_err();
924 assert!(matches!(err, Error::InvalidQueryContextExtension { .. }));
925 assert_eq!(err.status_code(), StatusCode::InvalidArguments);
926 }
927
928 #[test]
929 fn test_scan_request_from_query_context_rejects_invalid_sink_table_id() {
930 let region_id = test_region_id();
931 let query_ctx = QueryContextBuilder::default()
932 .extensions(HashMap::from([(
933 FLOW_SINK_TABLE_ID.to_string(),
934 "abc".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}