1use std::collections::HashMap;
16use std::path::PathBuf;
17
18use chrono::{DateTime, Utc};
19use common_base::memory_limit::MemoryLimit;
20use common_base::readable_size::ReadableSize;
21use datafusion::config::{ConfigEntry, ConfigExtension, ExtensionOptions};
22use serde::{Deserialize, Serialize};
23use session::context::QueryContextRef;
24use store_api::storage::RegionId;
25use table::metadata::TableId;
26
27use crate::error::{Error, InvalidQueryContextExtensionSnafu, Result};
28
29pub const FLOW_INCREMENTAL_AFTER_SEQS: &str = "flow.incremental_after_seqs";
30pub const FLOW_INCREMENTAL_MODE: &str = "flow.incremental_mode";
31pub const FLOW_RETURN_REGION_SEQ: &str = "flow.return_region_seq";
32pub const FLOW_SINK_TABLE_ID: &str = "flow.sink_table_id";
33pub const FLOW_SCHEDULED_TIME_MILLIS: &str = "flow.scheduled_time_millis";
37pub const QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN: &str =
39 "query.enable_remote_dynamic_filter_pushdown";
40
41pub const FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY: &str = "memtable_only";
42pub const FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE: &str = "sequence_range";
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "snake_case")]
47pub enum QuerySpillMode {
48 Default,
50 Custom,
52 Disabled,
54}
55
56#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59pub enum QuerySpillCompression {
60 Uncompressed,
62 Lz4Frame,
64 Zstd,
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(rename_all = "snake_case")]
71pub enum QueryMemoryPoolPolicy {
72 Greedy,
74 Fair,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
81#[serde(default)]
82pub struct QueryOptions {
83 pub parallelism: usize,
85 pub allow_query_fallback: bool,
87 pub memory_pool_size: MemoryLimit,
91 #[serde(skip)]
93 pub enable_per_region_metrics: bool,
94 pub experimental_spill_mode: QuerySpillMode,
99 pub experimental_spill_path: Option<PathBuf>,
102 pub experimental_spill_max_temp_directory_size: ReadableSize,
105 pub experimental_spill_compression: QuerySpillCompression,
108 pub experimental_memory_pool_policy: QueryMemoryPoolPolicy,
113}
114
115#[allow(clippy::derivable_impls)]
116impl Default for QueryOptions {
117 fn default() -> Self {
118 Self {
119 parallelism: 0,
120 allow_query_fallback: false,
121 memory_pool_size: MemoryLimit::default(),
122 enable_per_region_metrics: false,
123 experimental_spill_mode: QuerySpillMode::Default,
124 experimental_spill_path: None,
125 experimental_spill_max_temp_directory_size: ReadableSize::gb(1),
126 experimental_spill_compression: QuerySpillCompression::Uncompressed,
127 experimental_memory_pool_policy: QueryMemoryPoolPolicy::Greedy,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum FlowIncrementalMode {
134 MemtableOnly,
135 SequenceRange,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Default)]
139pub struct FlowQueryExtensions {
140 pub incremental_after_seqs: Option<HashMap<u64, u64>>,
142 pub incremental_mode: Option<FlowIncrementalMode>,
144 pub return_region_seq: bool,
146 pub sink_table_id: Option<TableId>,
148}
149
150impl FlowQueryExtensions {
151 pub fn parse_flow_extensions(extensions: &HashMap<String, String>) -> Result<Option<Self>> {
157 let has_flow_context = extensions.contains_key(FLOW_INCREMENTAL_AFTER_SEQS)
158 || extensions.contains_key(FLOW_INCREMENTAL_MODE)
159 || extensions.contains_key(FLOW_RETURN_REGION_SEQ)
160 || extensions.contains_key(FLOW_SINK_TABLE_ID);
161
162 if !has_flow_context {
163 return Ok(None);
164 }
165
166 let incremental_mode = extensions
167 .get(FLOW_INCREMENTAL_MODE)
168 .map(|value| match value.as_str() {
169 v if v.eq_ignore_ascii_case(FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY) => {
170 Ok(FlowIncrementalMode::MemtableOnly)
171 }
172 v if v.eq_ignore_ascii_case(FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE) => {
173 Ok(FlowIncrementalMode::SequenceRange)
174 }
175 _ => Err(invalid_query_context_extension(format!(
176 "Invalid value for {}: {}",
177 FLOW_INCREMENTAL_MODE, value
178 ))),
179 })
180 .transpose()?;
181
182 let incremental_after_seqs = extensions
183 .get(FLOW_INCREMENTAL_AFTER_SEQS)
184 .map(|value| parse_incremental_after_seqs(value.as_str()))
185 .transpose()?;
186
187 let return_region_seq = extensions
188 .get(FLOW_RETURN_REGION_SEQ)
189 .map(|value| parse_bool(FLOW_RETURN_REGION_SEQ, value.as_str()))
190 .transpose()?
191 .unwrap_or(false);
192
193 let sink_table_id = extensions
194 .get(FLOW_SINK_TABLE_ID)
195 .map(|value| {
196 value.parse::<TableId>().map_err(|_| {
197 invalid_query_context_extension(format!(
198 "Invalid value for {}: {}",
199 FLOW_SINK_TABLE_ID, value
200 ))
201 })
202 })
203 .transpose()?;
204
205 if matches!(
206 incremental_mode,
207 Some(FlowIncrementalMode::MemtableOnly | FlowIncrementalMode::SequenceRange)
208 ) {
209 let mode = if incremental_mode == Some(FlowIncrementalMode::MemtableOnly) {
210 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY
211 } else {
212 FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE
213 };
214 let after_seqs = incremental_after_seqs.as_ref().ok_or_else(|| {
215 invalid_query_context_extension(format!(
216 "{} is required when {}={}.",
217 FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, mode
218 ))
219 })?;
220 if after_seqs.is_empty() {
221 return Err(invalid_query_context_extension(format!(
222 "{} must not be empty when {}={}.",
223 FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, mode
224 )));
225 }
226 }
227
228 Ok(Some(Self {
229 incremental_after_seqs,
230 incremental_mode,
231 return_region_seq,
232 sink_table_id,
233 }))
234 }
235
236 pub fn validate_for_scan(&self, source_region_id: RegionId) -> Result<bool> {
237 if self.sink_table_id.is_some() && self.sink_table_id == Some(source_region_id.table_id()) {
238 return Ok(false);
239 }
240
241 if matches!(
242 self.incremental_mode,
243 Some(FlowIncrementalMode::MemtableOnly | FlowIncrementalMode::SequenceRange)
244 ) {
245 let mode = if self.incremental_mode == Some(FlowIncrementalMode::MemtableOnly) {
246 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY
247 } else {
248 FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE
249 };
250 let after_seqs = self.incremental_after_seqs.as_ref().ok_or_else(|| {
251 invalid_query_context_extension(format!(
252 "{} is required when {}={}.",
253 FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, mode
254 ))
255 })?;
256
257 if !after_seqs.contains_key(&source_region_id.as_u64()) {
258 return Err(invalid_query_context_extension(format!(
259 "Missing region {} in {} when {}={}.",
260 source_region_id, FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, mode
261 )));
262 }
263 }
264
265 Ok(self.incremental_after_seqs.is_some())
266 }
267
268 pub fn should_collect_region_watermark(&self) -> bool {
269 should_collect_region_watermark(
270 self.return_region_seq,
271 self.incremental_after_seqs.is_some(),
272 )
273 }
274}
275
276pub fn remote_dyn_filter_pushdown_enabled_from_extensions(
283 extensions: &HashMap<String, String>,
284) -> Result<bool> {
285 extensions
286 .get(QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN)
287 .map(|value| parse_bool(QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN, value.as_str()))
288 .transpose()
289 .map(|value| value.unwrap_or(true))
290}
291
292pub fn should_collect_region_watermark_from_extensions(
297 extensions: &HashMap<String, String>,
298) -> bool {
299 let return_region_seq = extensions
300 .get(FLOW_RETURN_REGION_SEQ)
301 .is_some_and(|value| value.eq_ignore_ascii_case("true"));
302 let has_incremental_after_seqs = extensions.contains_key(FLOW_INCREMENTAL_AFTER_SEQS);
303
304 should_collect_region_watermark(return_region_seq, has_incremental_after_seqs)
305}
306
307fn should_collect_region_watermark(
308 return_region_seq: bool,
309 has_incremental_after_seqs: bool,
310) -> bool {
311 return_region_seq || has_incremental_after_seqs
312}
313
314fn parse_incremental_after_seqs(value: &str) -> Result<HashMap<u64, u64>> {
315 let raw = serde_json::from_str::<HashMap<String, serde_json::Value>>(value).map_err(|e| {
316 invalid_query_context_extension(format!(
317 "Invalid JSON for {}: {} ({})",
318 FLOW_INCREMENTAL_AFTER_SEQS, value, e
319 ))
320 })?;
321
322 raw.into_iter()
323 .map(|(region_id, raw_seq)| {
324 let region_id = region_id.parse::<u64>().map_err(|_| {
325 invalid_query_context_extension(format!(
326 "Invalid region id in {}: {}",
327 FLOW_INCREMENTAL_AFTER_SEQS, region_id
328 ))
329 })?;
330
331 let seq = match raw_seq {
332 serde_json::Value::Number(num) => num.as_u64().ok_or_else(|| {
333 invalid_query_context_extension(format!(
334 "Invalid sequence value in {} for region {}: {}",
335 FLOW_INCREMENTAL_AFTER_SEQS, region_id, num
336 ))
337 })?,
338 serde_json::Value::String(s) => s.parse::<u64>().map_err(|_| {
339 invalid_query_context_extension(format!(
340 "Invalid sequence string in {} for region {}: {}",
341 FLOW_INCREMENTAL_AFTER_SEQS, region_id, s
342 ))
343 })?,
344 _ => {
345 return Err(invalid_query_context_extension(format!(
346 "Invalid sequence value type in {} for region {}",
347 FLOW_INCREMENTAL_AFTER_SEQS, region_id
348 )));
349 }
350 };
351
352 Ok((region_id, seq))
353 })
354 .collect()
355}
356
357fn parse_bool(option_name: &str, value: &str) -> Result<bool> {
358 match value {
359 v if v.eq_ignore_ascii_case("true") => Ok(true),
360 v if v.eq_ignore_ascii_case("false") => Ok(false),
361 _ => Err(invalid_query_context_extension(format!(
362 "Invalid value for {}: {}",
363 option_name, value
364 ))),
365 }
366}
367
368pub fn parse_scheduled_time_millis(extensions: &HashMap<String, String>) -> Result<Option<i64>> {
373 match extensions.get(FLOW_SCHEDULED_TIME_MILLIS) {
374 Some(val) => val.parse::<i64>().map(Some).map_err(|_| {
375 invalid_query_context_extension(format!(
376 "Invalid value for {}: {}",
377 FLOW_SCHEDULED_TIME_MILLIS, val
378 ))
379 }),
380 None => Ok(None),
381 }
382}
383
384pub fn parse_scheduled_time_datetime(
390 extensions: &HashMap<String, String>,
391) -> Result<Option<DateTime<Utc>>> {
392 match parse_scheduled_time_millis(extensions)? {
393 Some(millis) => DateTime::from_timestamp_millis(millis)
394 .map(Some)
395 .ok_or_else(|| {
396 invalid_query_context_extension(format!(
397 "Out-of-range timestamp for {}: {} ms",
398 FLOW_SCHEDULED_TIME_MILLIS, millis
399 ))
400 }),
401 None => Ok(None),
402 }
403}
404
405pub fn scheduled_time_from_ctx(query_ctx: &QueryContextRef) -> Option<DateTime<Utc>> {
410 let extensions = query_ctx.extensions();
411 parse_scheduled_time_datetime(&extensions).ok().flatten()
412}
413
414#[derive(Debug, Clone)]
418pub(crate) struct ScheduledTimeExtension {
419 pub(crate) scheduled_time: Option<DateTime<Utc>>,
420}
421
422impl ConfigExtension for ScheduledTimeExtension {
423 const PREFIX: &'static str = "flow_scheduled_time";
424}
425
426impl ExtensionOptions for ScheduledTimeExtension {
427 fn as_any(&self) -> &dyn std::any::Any {
428 self
429 }
430
431 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
432 self
433 }
434
435 fn cloned(&self) -> Box<dyn ExtensionOptions> {
436 Box::new(self.clone())
437 }
438
439 fn set(&mut self, key: &str, value: &str) -> datafusion::error::Result<()> {
440 Err(datafusion_common::DataFusionError::NotImplemented(format!(
441 "ScheduledTimeExtension does not support set key: {key} with value: {value}"
442 )))
443 }
444
445 fn entries(&self) -> Vec<ConfigEntry> {
446 vec![]
447 }
448}
449
450fn invalid_query_context_extension(reason: String) -> Error {
451 InvalidQueryContextExtensionSnafu { reason }.build()
452}
453
454#[cfg(test)]
455mod flow_extension_tests {
456 use super::*;
457
458 #[test]
459 fn test_parse_flow_extensions_returns_none_for_non_flow_query() {
460 let exts = HashMap::new();
461 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap();
462
463 assert_eq!(parsed, None);
464 }
465
466 #[test]
467 fn test_remote_dyn_filter_pushdown_enabled_from_extensions_defaults_true() {
468 assert!(remote_dyn_filter_pushdown_enabled_from_extensions(&HashMap::new()).unwrap());
469 }
470
471 #[test]
472 fn test_remote_dyn_filter_pushdown_enabled_from_extensions_parses_bool() {
473 let exts = HashMap::from([(
474 QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN.to_string(),
475 "false".to_string(),
476 )]);
477 assert!(!remote_dyn_filter_pushdown_enabled_from_extensions(&exts).unwrap());
478
479 let exts = HashMap::from([(
480 QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN.to_string(),
481 "true".to_string(),
482 )]);
483 assert!(remote_dyn_filter_pushdown_enabled_from_extensions(&exts).unwrap());
484 }
485
486 #[test]
487 fn test_remote_dyn_filter_pushdown_enabled_from_extensions_rejects_invalid_bool() {
488 let exts = HashMap::from([(
489 QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN.to_string(),
490 "invalid".to_string(),
491 )]);
492
493 let err = remote_dyn_filter_pushdown_enabled_from_extensions(&exts).unwrap_err();
494 assert!(format!("{err}").contains(QUERY_ENABLE_REMOTE_DYNAMIC_FILTER_PUSHDOWN));
495 }
496
497 #[test]
498 fn test_parse_flow_extensions_memtable_only_success() {
499 let exts = HashMap::from([
500 (
501 FLOW_INCREMENTAL_MODE.to_string(),
502 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY.to_string(),
503 ),
504 (
505 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
506 r#"{"1":10,"2":20}"#.to_string(),
507 ),
508 (FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string()),
509 (FLOW_SINK_TABLE_ID.to_string(), "1024".to_string()),
510 ]);
511
512 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
513 .unwrap()
514 .unwrap();
515 assert_eq!(
516 parsed.incremental_mode,
517 Some(FlowIncrementalMode::MemtableOnly)
518 );
519 assert_eq!(
520 parsed.incremental_after_seqs.unwrap(),
521 HashMap::from([(1, 10), (2, 20)])
522 );
523 assert!(parsed.return_region_seq);
524 assert_eq!(parsed.sink_table_id, Some(1024));
525 }
526
527 #[test]
528 fn test_parse_flow_extensions_sequence_range_success() {
529 let exts = HashMap::from([
530 (
531 FLOW_INCREMENTAL_MODE.to_string(),
532 FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
533 ),
534 (
535 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
536 r#"{"1":10}"#.to_string(),
537 ),
538 ]);
539
540 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
541 .unwrap()
542 .unwrap();
543 assert_eq!(
544 parsed.incremental_mode,
545 Some(FlowIncrementalMode::SequenceRange)
546 );
547 assert_eq!(
548 parsed.incremental_after_seqs,
549 Some(HashMap::from([(1, 10)]))
550 );
551 }
552
553 #[test]
554 fn test_parse_flow_extensions_sequence_range_rejects_empty_after_seqs() {
555 let exts = HashMap::from([
556 (
557 FLOW_INCREMENTAL_MODE.to_string(),
558 FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
559 ),
560 (FLOW_INCREMENTAL_AFTER_SEQS.to_string(), "{}".to_string()),
561 ]);
562
563 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
564 assert!(format!("{err}").contains(FLOW_INCREMENTAL_AFTER_SEQS));
565 }
566
567 #[test]
568 fn test_parse_flow_extensions_sequence_range_requires_after_seqs() {
569 let exts = HashMap::from([(
570 FLOW_INCREMENTAL_MODE.to_string(),
571 FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE.to_string(),
572 )]);
573
574 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
575 assert!(format!("{err}").contains(FLOW_INCREMENTAL_AFTER_SEQS));
576 }
577
578 #[test]
579 fn test_parse_flow_extensions_mode_requires_after_seqs() {
580 let exts = HashMap::from([(
581 FLOW_INCREMENTAL_MODE.to_string(),
582 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY.to_string(),
583 )]);
584
585 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
586 assert!(format!("{err}").contains(FLOW_INCREMENTAL_AFTER_SEQS));
587 }
588
589 #[test]
590 fn test_parse_flow_extensions_invalid_mode() {
591 let exts = HashMap::from([(FLOW_INCREMENTAL_MODE.to_string(), "foo".to_string())]);
592
593 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
594 assert!(format!("{err}").contains(FLOW_INCREMENTAL_MODE));
595 }
596
597 #[test]
598 fn test_parse_flow_extensions_invalid_after_seqs_json() {
599 let exts = HashMap::from([
600 (
601 FLOW_INCREMENTAL_MODE.to_string(),
602 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY.to_string(),
603 ),
604 (
605 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
606 "not-json".to_string(),
607 ),
608 ]);
609
610 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
611 assert!(format!("{err}").contains(FLOW_INCREMENTAL_AFTER_SEQS));
612 }
613
614 #[test]
615 fn test_parse_flow_extensions_after_seqs_string_values() {
616 let exts = HashMap::from([(
617 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
618 r#"{"1":"10","2":"20"}"#.to_string(),
619 )]);
620
621 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
622 .unwrap()
623 .unwrap();
624 assert_eq!(
625 parsed.incremental_after_seqs.unwrap(),
626 HashMap::from([(1, 10), (2, 20)])
627 );
628 }
629
630 #[test]
631 fn test_parse_flow_extensions_after_seqs_invalid_value_type() {
632 let exts = HashMap::from([(
633 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
634 r#"{"1":true}"#.to_string(),
635 )]);
636
637 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
638 assert!(format!("{err}").contains(FLOW_INCREMENTAL_AFTER_SEQS));
639 }
640
641 #[test]
642 fn test_parse_flow_extensions_invalid_sink_table_id() {
643 let exts = HashMap::from([(FLOW_SINK_TABLE_ID.to_string(), "x".to_string())]);
644
645 let err = FlowQueryExtensions::parse_flow_extensions(&exts).unwrap_err();
646 assert!(format!("{err}").contains(FLOW_SINK_TABLE_ID));
647 }
648
649 #[test]
650 fn test_validate_for_scan_missing_source_region() {
651 let source_region_id = RegionId::new(100, 2);
652 let existing_region_id = RegionId::new(100, 1);
653 let exts = HashMap::from([
654 (
655 FLOW_INCREMENTAL_MODE.to_string(),
656 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY.to_string(),
657 ),
658 (
659 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
660 format!(r#"{{"{}":10}}"#, existing_region_id.as_u64()),
661 ),
662 ]);
663
664 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
665 .unwrap()
666 .unwrap();
667 let err = parsed.validate_for_scan(source_region_id).unwrap_err();
668 assert!(format!("{err}").contains("Missing region"));
669 }
670
671 #[test]
672 fn test_validate_for_scan_sink_table_excluded() {
673 let source_region_id = RegionId::new(1024, 1);
674 let exts = HashMap::from([
675 (
676 FLOW_INCREMENTAL_MODE.to_string(),
677 FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY.to_string(),
678 ),
679 (
680 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
681 format!(r#"{{"{}":10}}"#, source_region_id.as_u64()),
682 ),
683 (FLOW_SINK_TABLE_ID.to_string(), "1024".to_string()),
684 ]);
685
686 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
687 .unwrap()
688 .unwrap();
689 let apply_incremental = parsed.validate_for_scan(source_region_id).unwrap();
690 assert!(!apply_incremental);
691 }
692
693 #[test]
694 fn test_should_collect_region_watermark_defaults_false() {
695 let parsed = FlowQueryExtensions::default();
696 assert!(!parsed.should_collect_region_watermark());
697 }
698
699 #[test]
700 fn test_should_collect_region_watermark_true_for_return_region_seq() {
701 let parsed = FlowQueryExtensions {
702 return_region_seq: true,
703 ..Default::default()
704 };
705 assert!(parsed.should_collect_region_watermark());
706 }
707
708 #[test]
709 fn test_should_collect_region_watermark_true_for_incremental_query() {
710 let parsed = FlowQueryExtensions {
711 incremental_after_seqs: Some(HashMap::from([(1, 10)])),
712 ..Default::default()
713 };
714 assert!(parsed.should_collect_region_watermark());
715 }
716
717 #[test]
718 fn test_should_collect_region_watermark_from_extensions() {
719 let exts = HashMap::from([(FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string())]);
720 assert!(should_collect_region_watermark_from_extensions(&exts));
721
722 let exts = HashMap::from([(
723 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
724 r#"{"1":10}"#.to_string(),
725 )]);
726 assert!(should_collect_region_watermark_from_extensions(&exts));
727
728 let exts = HashMap::from([(FLOW_RETURN_REGION_SEQ.to_string(), "false".to_string())]);
729 assert!(!should_collect_region_watermark_from_extensions(&exts));
730 assert!(!should_collect_region_watermark_from_extensions(
731 &HashMap::new()
732 ));
733 }
734
735 #[test]
736 fn test_parse_flow_extensions_return_region_seq_only_returns_some() {
737 let exts = HashMap::from([(FLOW_RETURN_REGION_SEQ.to_string(), "true".to_string())]);
738
739 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
740 .unwrap()
741 .unwrap();
742
743 assert!(parsed.return_region_seq);
744 }
745
746 #[test]
747 fn test_parse_flow_extensions_sink_table_only_returns_some() {
748 let exts = HashMap::from([(FLOW_SINK_TABLE_ID.to_string(), "1024".to_string())]);
749
750 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
751 .unwrap()
752 .unwrap();
753
754 assert_eq!(parsed.sink_table_id, Some(1024));
755 }
756
757 #[test]
758 fn test_parse_flow_extensions_incremental_after_seqs_only_returns_some() {
759 let exts = HashMap::from([(
760 FLOW_INCREMENTAL_AFTER_SEQS.to_string(),
761 r#"{"1":10}"#.to_string(),
762 )]);
763
764 let parsed = FlowQueryExtensions::parse_flow_extensions(&exts)
765 .unwrap()
766 .unwrap();
767
768 assert_eq!(
769 parsed.incremental_after_seqs,
770 Some(HashMap::from([(1, 10)]))
771 );
772 }
773
774 #[test]
777 fn test_parse_scheduled_time_millis_absent() {
778 let exts = HashMap::new();
779 assert_eq!(parse_scheduled_time_millis(&exts).unwrap(), None);
780 }
781
782 #[test]
783 fn test_parse_scheduled_time_millis_valid() {
784 let exts = HashMap::from([(
785 FLOW_SCHEDULED_TIME_MILLIS.to_string(),
786 "1700000000000".to_string(),
787 )]);
788 assert_eq!(
789 parse_scheduled_time_millis(&exts).unwrap(),
790 Some(1700000000000)
791 );
792 }
793
794 #[test]
795 fn test_parse_scheduled_time_millis_malformed() {
796 let exts = HashMap::from([(
797 FLOW_SCHEDULED_TIME_MILLIS.to_string(),
798 "not-a-number".to_string(),
799 )]);
800 let err = parse_scheduled_time_millis(&exts).unwrap_err();
801 assert!(format!("{err}").contains(FLOW_SCHEDULED_TIME_MILLIS));
802 }
803
804 #[test]
805 fn test_parse_scheduled_time_datetime_valid() {
806 let exts = HashMap::from([(
807 FLOW_SCHEDULED_TIME_MILLIS.to_string(),
808 "1700000000000".to_string(),
809 )]);
810 let dt = parse_scheduled_time_datetime(&exts).unwrap().unwrap();
811 assert_eq!(dt.timestamp_millis(), 1700000000000);
812 }
813
814 #[test]
815 fn test_parse_scheduled_time_datetime_negative_millis() {
816 let exts = HashMap::from([(FLOW_SCHEDULED_TIME_MILLIS.to_string(), "-1".to_string())]);
817 let dt = parse_scheduled_time_datetime(&exts).unwrap().unwrap();
818 assert_eq!(dt.timestamp_millis(), -1);
819 }
820
821 #[test]
822 fn test_parse_scheduled_time_datetime_out_of_range() {
823 let exts = HashMap::from([(FLOW_SCHEDULED_TIME_MILLIS.to_string(), i64::MAX.to_string())]);
825 let err = parse_scheduled_time_datetime(&exts).unwrap_err();
826 assert!(format!("{err}").contains("Out-of-range"));
827 }
828}
829
830#[cfg(test)]
831mod query_options_tests {
832 use super::*;
833
834 #[test]
835 fn test_parse_spill_options_from_toml() {
836 let toml_str = r#"
837experimental_spill_mode = "custom"
838experimental_spill_path = "/tmp/spill"
839experimental_spill_max_temp_directory_size = "50GiB"
840experimental_spill_compression = "zstd"
841experimental_memory_pool_policy = "fair"
842"#;
843 let opts: QueryOptions = toml::from_str(toml_str).unwrap();
844 assert_eq!(opts.experimental_spill_mode, QuerySpillMode::Custom);
845 assert_eq!(
846 opts.experimental_spill_path,
847 Some(PathBuf::from("/tmp/spill"))
848 );
849 assert_eq!(
850 opts.experimental_spill_max_temp_directory_size,
851 ReadableSize::gb(50)
852 );
853 assert_eq!(
854 opts.experimental_spill_compression,
855 QuerySpillCompression::Zstd
856 );
857 assert_eq!(
858 opts.experimental_memory_pool_policy,
859 QueryMemoryPoolPolicy::Fair
860 );
861 }
862
863 #[test]
864 fn test_parse_invalid_spill_option_values() {
865 for toml_str in [
866 r#"experimental_spill_mode = "invalid""#,
867 r#"experimental_spill_compression = "gzip""#,
868 r#"experimental_memory_pool_policy = "none""#,
869 ] {
870 assert!(
871 toml::from_str::<QueryOptions>(toml_str).is_err(),
872 "{toml_str}"
873 );
874 }
875 }
876}