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