1use crate::execution::TruncationReason;
8
9#[derive(Debug, Clone, Default)]
14pub enum StopCondition {
15 MaxRows(u64),
17 MaxBytes(u64),
25 SchemaStable {
28 consecutive_stable_rows: u64,
32 },
33 ConfidenceThreshold(f64),
38 MemoryPressure(f64),
42 Any(Vec<StopCondition>),
44 All(Vec<StopCondition>),
46 #[default]
48 Never,
49}
50
51impl StopCondition {
52 pub fn schema_inference() -> Self {
55 StopCondition::Any(vec![
56 StopCondition::MaxRows(10_000),
57 StopCondition::SchemaStable {
58 consecutive_stable_rows: 1_000,
59 },
60 ])
61 }
62
63 pub fn quality_sample() -> Self {
65 StopCondition::Any(vec![
66 StopCondition::MaxRows(50_000),
67 StopCondition::MaxBytes(50 * 1024 * 1024),
68 StopCondition::ConfidenceThreshold(0.95),
69 ])
70 }
71
72 pub fn max_rows(&self) -> Option<u64> {
87 match self {
88 StopCondition::MaxRows(n) => Some(*n),
89 StopCondition::Any(conditions) => {
91 conditions.iter().filter_map(StopCondition::max_rows).min()
92 }
93 StopCondition::All(conditions) => {
96 if conditions.is_empty() {
97 return None; }
99 conditions
100 .iter()
101 .map(StopCondition::max_rows)
102 .try_fold(0u64, |acc, cap| Some(acc.max(cap?)))
103 }
104 _ => None,
105 }
106 }
107
108 pub fn max_bytes(&self) -> Option<u64> {
116 match self {
117 StopCondition::MaxBytes(n) => Some(*n),
118 StopCondition::Any(conditions) => {
119 conditions.iter().filter_map(StopCondition::max_bytes).min()
120 }
121 StopCondition::All(conditions) => {
122 if conditions.is_empty() {
123 return None;
124 }
125 conditions
126 .iter()
127 .map(StopCondition::max_bytes)
128 .try_fold(0u64, |acc, cap| Some(acc.max(cap?)))
129 }
130 _ => None,
131 }
132 }
133
134 pub fn is_row_limit_only(&self) -> bool {
141 match self {
142 StopCondition::Never | StopCondition::MaxRows(_) => true,
143 StopCondition::Any(conditions) | StopCondition::All(conditions) => {
144 conditions.iter().all(StopCondition::is_row_limit_only)
145 }
146 _ => false,
147 }
148 }
149}
150
151pub struct StopEvaluator {
156 condition: StopCondition,
157 rows_processed: u64,
158 bytes_consumed: u64,
159 estimated_total_rows: Option<u64>,
160 triggered_reason: Option<TruncationReason>,
161}
162
163impl StopEvaluator {
164 pub fn new(condition: StopCondition) -> Self {
165 let condition = Self::clamp_thresholds(condition);
166 Self {
167 condition,
168 rows_processed: 0,
169 bytes_consumed: 0,
170 estimated_total_rows: None,
171 triggered_reason: None,
172 }
173 }
174
175 fn clamp_thresholds(condition: StopCondition) -> StopCondition {
178 match condition {
179 StopCondition::ConfidenceThreshold(t) => {
180 StopCondition::ConfidenceThreshold(t.clamp(0.0, 1.0))
181 }
182 StopCondition::MemoryPressure(t) => StopCondition::MemoryPressure(t.clamp(0.0, 1.0)),
183 StopCondition::Any(cs) => {
184 StopCondition::Any(cs.into_iter().map(Self::clamp_thresholds).collect())
185 }
186 StopCondition::All(cs) => {
187 StopCondition::All(cs.into_iter().map(Self::clamp_thresholds).collect())
188 }
189 other => other,
190 }
191 }
192
193 pub fn with_estimated_total(mut self, rows: u64) -> Self {
195 self.estimated_total_rows = Some(rows);
196 self
197 }
198
199 pub fn update(&mut self, chunk_rows: u64, chunk_bytes: u64, memory_fraction: f64) -> bool {
207 self.rows_processed += chunk_rows;
208 self.bytes_consumed += chunk_bytes;
209
210 if self.triggered_reason.is_some() {
211 return true;
212 }
213
214 let reason = evaluate(
215 &self.condition,
216 self.rows_processed,
217 self.bytes_consumed,
218 memory_fraction,
219 self.estimated_total_rows,
220 );
221
222 if reason.is_some() {
223 self.triggered_reason = reason;
224 true
225 } else {
226 false
227 }
228 }
229
230 pub fn should_stop(&self) -> bool {
232 self.triggered_reason.is_some()
233 }
234
235 pub fn truncation_reason(&self) -> Option<TruncationReason> {
237 self.triggered_reason.clone()
238 }
239
240 pub fn rows_processed(&self) -> u64 {
242 self.rows_processed
243 }
244
245 pub fn bytes_consumed(&self) -> u64 {
247 self.bytes_consumed
248 }
249}
250
251fn evaluate(
254 condition: &StopCondition,
255 rows: u64,
256 bytes: u64,
257 memory_fraction: f64,
258 estimated_total: Option<u64>,
259) -> Option<TruncationReason> {
260 match condition {
261 StopCondition::MaxRows(limit) => {
262 if rows >= *limit {
263 Some(TruncationReason::MaxRows(*limit))
264 } else {
265 None
266 }
267 }
268 StopCondition::MaxBytes(limit) => {
269 if bytes >= *limit {
270 Some(TruncationReason::MaxBytes(*limit))
271 } else {
272 None
273 }
274 }
275 StopCondition::SchemaStable { .. } => {
276 None
281 }
282 StopCondition::ConfidenceThreshold(threshold) => {
283 if let Some(total) = estimated_total
284 && total > 0
285 {
286 let confidence = rows as f64 / total as f64;
287 if confidence >= *threshold {
288 return Some(TruncationReason::StopCondition(format!(
289 "confidence_threshold({})",
290 threshold
291 )));
292 }
293 }
294 None
295 }
296 StopCondition::MemoryPressure(threshold) => {
297 if memory_fraction >= *threshold {
298 Some(TruncationReason::MemoryPressure)
299 } else {
300 None
301 }
302 }
303 StopCondition::Any(conditions) => {
304 for c in conditions {
305 if let Some(reason) = evaluate(c, rows, bytes, memory_fraction, estimated_total) {
306 return Some(reason);
307 }
308 }
309 None
310 }
311 StopCondition::All(conditions) => {
312 if conditions.is_empty() {
313 return None;
314 }
315 let mut first_reason = None;
317 for c in conditions {
318 let reason = evaluate(c, rows, bytes, memory_fraction, estimated_total)?;
319 if first_reason.is_none() {
320 first_reason = Some(reason);
321 }
322 }
323 first_reason
324 }
325 StopCondition::Never => None,
326 }
327}
328
329pub fn schema_stable_threshold(condition: &StopCondition) -> Option<u64> {
333 match condition {
334 StopCondition::SchemaStable {
335 consecutive_stable_rows,
336 } => Some(*consecutive_stable_rows),
337 StopCondition::Any(conditions) | StopCondition::All(conditions) => {
338 conditions.iter().find_map(schema_stable_threshold)
339 }
340 _ => None,
341 }
342}
343
344pub struct SchemaStabilityTracker {
351 threshold: u64,
352 consecutive_stable: u64,
353 last_fingerprint: Option<u64>,
354}
355
356impl SchemaStabilityTracker {
357 pub fn from_condition(condition: &StopCondition) -> Option<Self> {
360 schema_stable_threshold(condition).map(|threshold| Self {
361 threshold,
362 consecutive_stable: 0,
363 last_fingerprint: None,
364 })
365 }
366
367 pub fn update(&mut self, fingerprint: u64, chunk_rows: u64) -> bool {
371 match self.last_fingerprint {
372 Some(prev) if prev == fingerprint => {
373 self.consecutive_stable += chunk_rows;
374 }
375 _ => {
376 self.consecutive_stable = chunk_rows;
377 self.last_fingerprint = Some(fingerprint);
378 }
379 }
380 self.consecutive_stable >= self.threshold
381 }
382
383 pub fn truncation_reason(&self) -> TruncationReason {
385 TruncationReason::StopCondition(format!("schema_stable({})", self.threshold))
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn max_bytes_follows_the_any_and_all_rules() {
395 assert_eq!(StopCondition::MaxBytes(10).max_bytes(), Some(10));
396 assert_eq!(StopCondition::MaxRows(10).max_bytes(), None);
397 let any = StopCondition::Any(vec![
398 StopCondition::MaxBytes(20),
399 StopCondition::MaxRows(5),
400 StopCondition::MaxBytes(10),
401 ]);
402 assert_eq!(any.max_bytes(), Some(10));
403 let all = StopCondition::All(vec![
404 StopCondition::MaxBytes(10),
405 StopCondition::MaxBytes(20),
406 ]);
407 assert_eq!(all.max_bytes(), Some(20));
408 let mixed =
411 StopCondition::All(vec![StopCondition::MaxBytes(10), StopCondition::MaxRows(5)]);
412 assert_eq!(mixed.max_bytes(), None);
413 assert_eq!(StopCondition::All(vec![]).max_bytes(), None);
414 }
415
416 #[test]
417 fn test_max_rows_leaf_and_never() {
418 assert_eq!(StopCondition::MaxRows(10).max_rows(), Some(10));
419 assert_eq!(StopCondition::Never.max_rows(), None);
420 assert_eq!(StopCondition::MaxBytes(10).max_rows(), None);
421 }
422
423 #[test]
424 fn test_max_rows_any_takes_the_earliest_cap() {
425 let condition =
427 StopCondition::Any(vec![StopCondition::MaxRows(20), StopCondition::MaxRows(10)]);
428 assert_eq!(condition.max_rows(), Some(10));
429
430 let flipped =
432 StopCondition::Any(vec![StopCondition::MaxRows(10), StopCondition::MaxRows(20)]);
433 assert_eq!(flipped.max_rows(), Some(10));
434
435 let with_never = StopCondition::Any(vec![StopCondition::Never, StopCondition::MaxRows(10)]);
437 assert_eq!(with_never.max_rows(), Some(10));
438
439 let mixed = StopCondition::Any(vec![
441 StopCondition::MaxBytes(1024),
442 StopCondition::MaxRows(10),
443 ]);
444 assert_eq!(mixed.max_rows(), Some(10));
445 }
446
447 #[test]
448 fn test_max_rows_all_needs_every_child_row_triggerable() {
449 let condition =
451 StopCondition::All(vec![StopCondition::MaxRows(10), StopCondition::MaxRows(20)]);
452 assert_eq!(condition.max_rows(), Some(20));
453
454 let with_bytes = StopCondition::All(vec![
456 StopCondition::MaxRows(10),
457 StopCondition::MaxBytes(1024),
458 ]);
459 assert_eq!(with_bytes.max_rows(), None);
460
461 let with_never = StopCondition::All(vec![StopCondition::MaxRows(10), StopCondition::Never]);
462 assert_eq!(with_never.max_rows(), None);
463
464 assert_eq!(StopCondition::All(vec![]).max_rows(), None);
466 assert_eq!(StopCondition::Any(vec![]).max_rows(), None);
467 }
468
469 #[test]
470 fn test_is_row_limit_only_recurses() {
471 assert!(StopCondition::Never.is_row_limit_only());
472 assert!(StopCondition::MaxRows(10).is_row_limit_only());
473 assert!(!StopCondition::MaxBytes(10).is_row_limit_only());
474
475 assert!(
477 StopCondition::Any(vec![StopCondition::MaxRows(10), StopCondition::MaxRows(20)])
478 .is_row_limit_only()
479 );
480 assert!(
481 StopCondition::All(vec![StopCondition::Never, StopCondition::MaxRows(20)])
482 .is_row_limit_only()
483 );
484
485 assert!(
487 !StopCondition::Any(vec![
488 StopCondition::MaxRows(10),
489 StopCondition::MaxBytes(1024)
490 ])
491 .is_row_limit_only()
492 );
493
494 assert!(!StopCondition::schema_inference().is_row_limit_only());
496 assert_eq!(StopCondition::schema_inference().max_rows(), Some(10_000));
498 }
499
500 #[test]
501 fn test_max_rows_stops() {
502 let mut eval = StopEvaluator::new(StopCondition::MaxRows(100));
503 assert!(!eval.update(50, 0, 0.0));
504 assert!(!eval.should_stop());
505 assert!(eval.update(50, 0, 0.0));
506 assert!(eval.should_stop());
507 assert_eq!(
508 eval.truncation_reason(),
509 Some(TruncationReason::MaxRows(100))
510 );
511 }
512
513 #[test]
514 fn test_max_bytes_stops() {
515 let mut eval = StopEvaluator::new(StopCondition::MaxBytes(1000));
516 assert!(!eval.update(10, 500, 0.0));
517 assert!(eval.update(10, 600, 0.0));
518 assert_eq!(
519 eval.truncation_reason(),
520 Some(TruncationReason::MaxBytes(1000))
521 );
522 }
523
524 #[test]
525 fn test_memory_pressure_stops() {
526 let mut eval = StopEvaluator::new(StopCondition::MemoryPressure(0.9));
527 assert!(!eval.update(100, 0, 0.5));
528 assert!(eval.update(100, 0, 0.95));
529 assert_eq!(
530 eval.truncation_reason(),
531 Some(TruncationReason::MemoryPressure)
532 );
533 }
534
535 #[test]
536 fn test_confidence_threshold_without_estimate() {
537 let mut eval = StopEvaluator::new(StopCondition::ConfidenceThreshold(0.95));
539 assert!(!eval.update(100_000, 0, 0.0));
540 assert!(!eval.should_stop());
541 }
542
543 #[test]
544 fn test_confidence_threshold_with_estimate() {
545 let mut eval =
546 StopEvaluator::new(StopCondition::ConfidenceThreshold(0.95)).with_estimated_total(1000);
547 assert!(!eval.update(900, 0, 0.0));
548 assert!(eval.update(100, 0, 0.0)); }
550
551 #[test]
552 fn test_never_never_stops() {
553 let mut eval = StopEvaluator::new(StopCondition::Never);
554 for _ in 0..100 {
555 assert!(!eval.update(1_000_000, 1_000_000, 1.0));
556 }
557 assert!(!eval.should_stop());
558 }
559
560 #[test]
561 fn test_any_stops_on_first() {
562 let condition = StopCondition::Any(vec![
563 StopCondition::MaxRows(100),
564 StopCondition::MaxBytes(1_000_000),
565 ]);
566 let mut eval = StopEvaluator::new(condition);
567 assert!(eval.update(100, 500, 0.0));
569 assert_eq!(
570 eval.truncation_reason(),
571 Some(TruncationReason::MaxRows(100))
572 );
573 }
574
575 #[test]
576 fn test_all_requires_all() {
577 let condition = StopCondition::All(vec![
578 StopCondition::MaxRows(100),
579 StopCondition::MaxBytes(1000),
580 ]);
581 let mut eval = StopEvaluator::new(condition);
582 assert!(!eval.update(100, 500, 0.0));
584 assert!(eval.update(0, 600, 0.0));
586 assert_eq!(
587 eval.truncation_reason(),
588 Some(TruncationReason::MaxRows(100))
589 );
590 }
591
592 #[test]
593 fn test_all_empty_never_triggers() {
594 let mut eval = StopEvaluator::new(StopCondition::All(vec![]));
595 assert!(!eval.update(100, 100, 1.0));
596 }
597
598 #[test]
599 fn test_convenience_schema_inference() {
600 let condition = StopCondition::schema_inference();
601 match &condition {
602 StopCondition::Any(conditions) => {
603 assert_eq!(conditions.len(), 2);
604 assert!(matches!(conditions[0], StopCondition::MaxRows(10_000)));
605 assert!(matches!(
606 conditions[1],
607 StopCondition::SchemaStable {
608 consecutive_stable_rows: 1_000
609 }
610 ));
611 }
612 _ => panic!("Expected Any variant"),
613 }
614 }
615
616 #[test]
617 fn test_convenience_quality_sample() {
618 let condition = StopCondition::quality_sample();
619 match &condition {
620 StopCondition::Any(conditions) => {
621 assert_eq!(conditions.len(), 3);
622 assert!(matches!(conditions[0], StopCondition::MaxRows(50_000)));
623 assert!(matches!(conditions[1], StopCondition::MaxBytes(52_428_800)));
624 }
625 _ => panic!("Expected Any variant"),
626 }
627 }
628
629 #[test]
630 fn test_once_triggered_stays_triggered() {
631 let mut eval = StopEvaluator::new(StopCondition::MaxRows(10));
632 assert!(eval.update(10, 0, 0.0));
633 assert!(eval.update(5, 0, 0.0));
635 assert!(eval.should_stop());
636 }
637
638 #[test]
639 fn test_schema_stability_tracker() {
640 let condition = StopCondition::SchemaStable {
641 consecutive_stable_rows: 3,
642 };
643 let mut tracker = SchemaStabilityTracker::from_condition(&condition).unwrap();
644
645 let fp: u64 = 0xABCD;
646 assert!(!tracker.update(fp, 1)); assert!(!tracker.update(fp, 1)); assert!(tracker.update(fp, 1)); }
650
651 #[test]
652 fn test_schema_stability_tracker_resets_on_change() {
653 let condition = StopCondition::SchemaStable {
654 consecutive_stable_rows: 3,
655 };
656 let mut tracker = SchemaStabilityTracker::from_condition(&condition).unwrap();
657
658 let fp1: u64 = 0x1111;
659 let fp2: u64 = 0x2222;
660
661 assert!(!tracker.update(fp1, 1)); assert!(!tracker.update(fp1, 1)); assert!(!tracker.update(fp2, 1)); assert!(!tracker.update(fp2, 1)); assert!(tracker.update(fp2, 1)); }
668
669 #[test]
670 fn test_schema_stable_threshold_extraction() {
671 assert_eq!(schema_stable_threshold(&StopCondition::Never), None);
672 assert_eq!(
673 schema_stable_threshold(&StopCondition::SchemaStable {
674 consecutive_stable_rows: 500
675 }),
676 Some(500)
677 );
678 let nested = StopCondition::Any(vec![
680 StopCondition::MaxRows(100),
681 StopCondition::SchemaStable {
682 consecutive_stable_rows: 200,
683 },
684 ]);
685 assert_eq!(schema_stable_threshold(&nested), Some(200));
686 }
687
688 #[test]
689 fn test_rows_and_bytes_accessors() {
690 let mut eval = StopEvaluator::new(StopCondition::Never);
691 eval.update(100, 500, 0.0);
692 eval.update(200, 1000, 0.0);
693 assert_eq!(eval.rows_processed(), 300);
694 assert_eq!(eval.bytes_consumed(), 1500);
695 }
696}