1use crate::execution::TruncationReason;
8
9#[derive(Debug, Clone, Default)]
14pub enum StopCondition {
15 MaxRows(u64),
17 MaxBytes(u64),
19 SchemaStable {
22 consecutive_stable_rows: u64,
26 },
27 ConfidenceThreshold(f64),
32 MemoryPressure(f64),
36 Any(Vec<StopCondition>),
38 All(Vec<StopCondition>),
40 #[default]
42 Never,
43}
44
45impl StopCondition {
46 pub fn schema_inference() -> Self {
49 StopCondition::Any(vec![
50 StopCondition::MaxRows(10_000),
51 StopCondition::SchemaStable {
52 consecutive_stable_rows: 1_000,
53 },
54 ])
55 }
56
57 pub fn quality_sample() -> Self {
59 StopCondition::Any(vec![
60 StopCondition::MaxRows(50_000),
61 StopCondition::MaxBytes(50 * 1024 * 1024),
62 StopCondition::ConfidenceThreshold(0.95),
63 ])
64 }
65
66 pub fn max_rows(&self) -> Option<u64> {
81 match self {
82 StopCondition::MaxRows(n) => Some(*n),
83 StopCondition::Any(conditions) => {
85 conditions.iter().filter_map(StopCondition::max_rows).min()
86 }
87 StopCondition::All(conditions) => {
90 if conditions.is_empty() {
91 return None; }
93 conditions
94 .iter()
95 .map(StopCondition::max_rows)
96 .try_fold(0u64, |acc, cap| Some(acc.max(cap?)))
97 }
98 _ => None,
99 }
100 }
101
102 pub fn is_row_limit_only(&self) -> bool {
109 match self {
110 StopCondition::Never | StopCondition::MaxRows(_) => true,
111 StopCondition::Any(conditions) | StopCondition::All(conditions) => {
112 conditions.iter().all(StopCondition::is_row_limit_only)
113 }
114 _ => false,
115 }
116 }
117}
118
119pub struct StopEvaluator {
124 condition: StopCondition,
125 rows_processed: u64,
126 bytes_consumed: u64,
127 estimated_total_rows: Option<u64>,
128 triggered_reason: Option<TruncationReason>,
129}
130
131impl StopEvaluator {
132 pub fn new(condition: StopCondition) -> Self {
133 let condition = Self::clamp_thresholds(condition);
134 Self {
135 condition,
136 rows_processed: 0,
137 bytes_consumed: 0,
138 estimated_total_rows: None,
139 triggered_reason: None,
140 }
141 }
142
143 fn clamp_thresholds(condition: StopCondition) -> StopCondition {
146 match condition {
147 StopCondition::ConfidenceThreshold(t) => {
148 StopCondition::ConfidenceThreshold(t.clamp(0.0, 1.0))
149 }
150 StopCondition::MemoryPressure(t) => StopCondition::MemoryPressure(t.clamp(0.0, 1.0)),
151 StopCondition::Any(cs) => {
152 StopCondition::Any(cs.into_iter().map(Self::clamp_thresholds).collect())
153 }
154 StopCondition::All(cs) => {
155 StopCondition::All(cs.into_iter().map(Self::clamp_thresholds).collect())
156 }
157 other => other,
158 }
159 }
160
161 pub fn with_estimated_total(mut self, rows: u64) -> Self {
163 self.estimated_total_rows = Some(rows);
164 self
165 }
166
167 pub fn update(&mut self, chunk_rows: u64, chunk_bytes: u64, memory_fraction: f64) -> bool {
175 self.rows_processed += chunk_rows;
176 self.bytes_consumed += chunk_bytes;
177
178 if self.triggered_reason.is_some() {
179 return true;
180 }
181
182 let reason = evaluate(
183 &self.condition,
184 self.rows_processed,
185 self.bytes_consumed,
186 memory_fraction,
187 self.estimated_total_rows,
188 );
189
190 if reason.is_some() {
191 self.triggered_reason = reason;
192 true
193 } else {
194 false
195 }
196 }
197
198 pub fn should_stop(&self) -> bool {
200 self.triggered_reason.is_some()
201 }
202
203 pub fn truncation_reason(&self) -> Option<TruncationReason> {
205 self.triggered_reason.clone()
206 }
207
208 pub fn rows_processed(&self) -> u64 {
210 self.rows_processed
211 }
212
213 pub fn bytes_consumed(&self) -> u64 {
215 self.bytes_consumed
216 }
217}
218
219fn evaluate(
222 condition: &StopCondition,
223 rows: u64,
224 bytes: u64,
225 memory_fraction: f64,
226 estimated_total: Option<u64>,
227) -> Option<TruncationReason> {
228 match condition {
229 StopCondition::MaxRows(limit) => {
230 if rows >= *limit {
231 Some(TruncationReason::MaxRows(*limit))
232 } else {
233 None
234 }
235 }
236 StopCondition::MaxBytes(limit) => {
237 if bytes >= *limit {
238 Some(TruncationReason::MaxBytes(*limit))
239 } else {
240 None
241 }
242 }
243 StopCondition::SchemaStable { .. } => {
244 None
249 }
250 StopCondition::ConfidenceThreshold(threshold) => {
251 if let Some(total) = estimated_total
252 && total > 0
253 {
254 let confidence = rows as f64 / total as f64;
255 if confidence >= *threshold {
256 return Some(TruncationReason::StopCondition(format!(
257 "confidence_threshold({})",
258 threshold
259 )));
260 }
261 }
262 None
263 }
264 StopCondition::MemoryPressure(threshold) => {
265 if memory_fraction >= *threshold {
266 Some(TruncationReason::MemoryPressure)
267 } else {
268 None
269 }
270 }
271 StopCondition::Any(conditions) => {
272 for c in conditions {
273 if let Some(reason) = evaluate(c, rows, bytes, memory_fraction, estimated_total) {
274 return Some(reason);
275 }
276 }
277 None
278 }
279 StopCondition::All(conditions) => {
280 if conditions.is_empty() {
281 return None;
282 }
283 let mut first_reason = None;
285 for c in conditions {
286 let reason = evaluate(c, rows, bytes, memory_fraction, estimated_total)?;
287 if first_reason.is_none() {
288 first_reason = Some(reason);
289 }
290 }
291 first_reason
292 }
293 StopCondition::Never => None,
294 }
295}
296
297pub fn schema_stable_threshold(condition: &StopCondition) -> Option<u64> {
301 match condition {
302 StopCondition::SchemaStable {
303 consecutive_stable_rows,
304 } => Some(*consecutive_stable_rows),
305 StopCondition::Any(conditions) | StopCondition::All(conditions) => {
306 conditions.iter().find_map(schema_stable_threshold)
307 }
308 _ => None,
309 }
310}
311
312pub struct SchemaStabilityTracker {
319 threshold: u64,
320 consecutive_stable: u64,
321 last_fingerprint: Option<u64>,
322}
323
324impl SchemaStabilityTracker {
325 pub fn from_condition(condition: &StopCondition) -> Option<Self> {
328 schema_stable_threshold(condition).map(|threshold| Self {
329 threshold,
330 consecutive_stable: 0,
331 last_fingerprint: None,
332 })
333 }
334
335 pub fn update(&mut self, fingerprint: u64, chunk_rows: u64) -> bool {
339 match self.last_fingerprint {
340 Some(prev) if prev == fingerprint => {
341 self.consecutive_stable += chunk_rows;
342 }
343 _ => {
344 self.consecutive_stable = chunk_rows;
345 self.last_fingerprint = Some(fingerprint);
346 }
347 }
348 self.consecutive_stable >= self.threshold
349 }
350
351 pub fn truncation_reason(&self) -> TruncationReason {
353 TruncationReason::StopCondition(format!("schema_stable({})", self.threshold))
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_max_rows_leaf_and_never() {
363 assert_eq!(StopCondition::MaxRows(10).max_rows(), Some(10));
364 assert_eq!(StopCondition::Never.max_rows(), None);
365 assert_eq!(StopCondition::MaxBytes(10).max_rows(), None);
366 }
367
368 #[test]
369 fn test_max_rows_any_takes_the_earliest_cap() {
370 let condition =
372 StopCondition::Any(vec![StopCondition::MaxRows(20), StopCondition::MaxRows(10)]);
373 assert_eq!(condition.max_rows(), Some(10));
374
375 let flipped =
377 StopCondition::Any(vec![StopCondition::MaxRows(10), StopCondition::MaxRows(20)]);
378 assert_eq!(flipped.max_rows(), Some(10));
379
380 let with_never = StopCondition::Any(vec![StopCondition::Never, StopCondition::MaxRows(10)]);
382 assert_eq!(with_never.max_rows(), Some(10));
383
384 let mixed = StopCondition::Any(vec![
386 StopCondition::MaxBytes(1024),
387 StopCondition::MaxRows(10),
388 ]);
389 assert_eq!(mixed.max_rows(), Some(10));
390 }
391
392 #[test]
393 fn test_max_rows_all_needs_every_child_row_triggerable() {
394 let condition =
396 StopCondition::All(vec![StopCondition::MaxRows(10), StopCondition::MaxRows(20)]);
397 assert_eq!(condition.max_rows(), Some(20));
398
399 let with_bytes = StopCondition::All(vec![
401 StopCondition::MaxRows(10),
402 StopCondition::MaxBytes(1024),
403 ]);
404 assert_eq!(with_bytes.max_rows(), None);
405
406 let with_never = StopCondition::All(vec![StopCondition::MaxRows(10), StopCondition::Never]);
407 assert_eq!(with_never.max_rows(), None);
408
409 assert_eq!(StopCondition::All(vec![]).max_rows(), None);
411 assert_eq!(StopCondition::Any(vec![]).max_rows(), None);
412 }
413
414 #[test]
415 fn test_is_row_limit_only_recurses() {
416 assert!(StopCondition::Never.is_row_limit_only());
417 assert!(StopCondition::MaxRows(10).is_row_limit_only());
418 assert!(!StopCondition::MaxBytes(10).is_row_limit_only());
419
420 assert!(
422 StopCondition::Any(vec![StopCondition::MaxRows(10), StopCondition::MaxRows(20)])
423 .is_row_limit_only()
424 );
425 assert!(
426 StopCondition::All(vec![StopCondition::Never, StopCondition::MaxRows(20)])
427 .is_row_limit_only()
428 );
429
430 assert!(
432 !StopCondition::Any(vec![
433 StopCondition::MaxRows(10),
434 StopCondition::MaxBytes(1024)
435 ])
436 .is_row_limit_only()
437 );
438
439 assert!(!StopCondition::schema_inference().is_row_limit_only());
441 assert_eq!(StopCondition::schema_inference().max_rows(), Some(10_000));
443 }
444
445 #[test]
446 fn test_max_rows_stops() {
447 let mut eval = StopEvaluator::new(StopCondition::MaxRows(100));
448 assert!(!eval.update(50, 0, 0.0));
449 assert!(!eval.should_stop());
450 assert!(eval.update(50, 0, 0.0));
451 assert!(eval.should_stop());
452 assert_eq!(
453 eval.truncation_reason(),
454 Some(TruncationReason::MaxRows(100))
455 );
456 }
457
458 #[test]
459 fn test_max_bytes_stops() {
460 let mut eval = StopEvaluator::new(StopCondition::MaxBytes(1000));
461 assert!(!eval.update(10, 500, 0.0));
462 assert!(eval.update(10, 600, 0.0));
463 assert_eq!(
464 eval.truncation_reason(),
465 Some(TruncationReason::MaxBytes(1000))
466 );
467 }
468
469 #[test]
470 fn test_memory_pressure_stops() {
471 let mut eval = StopEvaluator::new(StopCondition::MemoryPressure(0.9));
472 assert!(!eval.update(100, 0, 0.5));
473 assert!(eval.update(100, 0, 0.95));
474 assert_eq!(
475 eval.truncation_reason(),
476 Some(TruncationReason::MemoryPressure)
477 );
478 }
479
480 #[test]
481 fn test_confidence_threshold_without_estimate() {
482 let mut eval = StopEvaluator::new(StopCondition::ConfidenceThreshold(0.95));
484 assert!(!eval.update(100_000, 0, 0.0));
485 assert!(!eval.should_stop());
486 }
487
488 #[test]
489 fn test_confidence_threshold_with_estimate() {
490 let mut eval =
491 StopEvaluator::new(StopCondition::ConfidenceThreshold(0.95)).with_estimated_total(1000);
492 assert!(!eval.update(900, 0, 0.0));
493 assert!(eval.update(100, 0, 0.0)); }
495
496 #[test]
497 fn test_never_never_stops() {
498 let mut eval = StopEvaluator::new(StopCondition::Never);
499 for _ in 0..100 {
500 assert!(!eval.update(1_000_000, 1_000_000, 1.0));
501 }
502 assert!(!eval.should_stop());
503 }
504
505 #[test]
506 fn test_any_stops_on_first() {
507 let condition = StopCondition::Any(vec![
508 StopCondition::MaxRows(100),
509 StopCondition::MaxBytes(1_000_000),
510 ]);
511 let mut eval = StopEvaluator::new(condition);
512 assert!(eval.update(100, 500, 0.0));
514 assert_eq!(
515 eval.truncation_reason(),
516 Some(TruncationReason::MaxRows(100))
517 );
518 }
519
520 #[test]
521 fn test_all_requires_all() {
522 let condition = StopCondition::All(vec![
523 StopCondition::MaxRows(100),
524 StopCondition::MaxBytes(1000),
525 ]);
526 let mut eval = StopEvaluator::new(condition);
527 assert!(!eval.update(100, 500, 0.0));
529 assert!(eval.update(0, 600, 0.0));
531 assert_eq!(
532 eval.truncation_reason(),
533 Some(TruncationReason::MaxRows(100))
534 );
535 }
536
537 #[test]
538 fn test_all_empty_never_triggers() {
539 let mut eval = StopEvaluator::new(StopCondition::All(vec![]));
540 assert!(!eval.update(100, 100, 1.0));
541 }
542
543 #[test]
544 fn test_convenience_schema_inference() {
545 let condition = StopCondition::schema_inference();
546 match &condition {
547 StopCondition::Any(conditions) => {
548 assert_eq!(conditions.len(), 2);
549 assert!(matches!(conditions[0], StopCondition::MaxRows(10_000)));
550 assert!(matches!(
551 conditions[1],
552 StopCondition::SchemaStable {
553 consecutive_stable_rows: 1_000
554 }
555 ));
556 }
557 _ => panic!("Expected Any variant"),
558 }
559 }
560
561 #[test]
562 fn test_convenience_quality_sample() {
563 let condition = StopCondition::quality_sample();
564 match &condition {
565 StopCondition::Any(conditions) => {
566 assert_eq!(conditions.len(), 3);
567 assert!(matches!(conditions[0], StopCondition::MaxRows(50_000)));
568 assert!(matches!(conditions[1], StopCondition::MaxBytes(52_428_800)));
569 }
570 _ => panic!("Expected Any variant"),
571 }
572 }
573
574 #[test]
575 fn test_once_triggered_stays_triggered() {
576 let mut eval = StopEvaluator::new(StopCondition::MaxRows(10));
577 assert!(eval.update(10, 0, 0.0));
578 assert!(eval.update(5, 0, 0.0));
580 assert!(eval.should_stop());
581 }
582
583 #[test]
584 fn test_schema_stability_tracker() {
585 let condition = StopCondition::SchemaStable {
586 consecutive_stable_rows: 3,
587 };
588 let mut tracker = SchemaStabilityTracker::from_condition(&condition).unwrap();
589
590 let fp: u64 = 0xABCD;
591 assert!(!tracker.update(fp, 1)); assert!(!tracker.update(fp, 1)); assert!(tracker.update(fp, 1)); }
595
596 #[test]
597 fn test_schema_stability_tracker_resets_on_change() {
598 let condition = StopCondition::SchemaStable {
599 consecutive_stable_rows: 3,
600 };
601 let mut tracker = SchemaStabilityTracker::from_condition(&condition).unwrap();
602
603 let fp1: u64 = 0x1111;
604 let fp2: u64 = 0x2222;
605
606 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)); }
613
614 #[test]
615 fn test_schema_stable_threshold_extraction() {
616 assert_eq!(schema_stable_threshold(&StopCondition::Never), None);
617 assert_eq!(
618 schema_stable_threshold(&StopCondition::SchemaStable {
619 consecutive_stable_rows: 500
620 }),
621 Some(500)
622 );
623 let nested = StopCondition::Any(vec![
625 StopCondition::MaxRows(100),
626 StopCondition::SchemaStable {
627 consecutive_stable_rows: 200,
628 },
629 ]);
630 assert_eq!(schema_stable_threshold(&nested), Some(200));
631 }
632
633 #[test]
634 fn test_rows_and_bytes_accessors() {
635 let mut eval = StopEvaluator::new(StopCondition::Never);
636 eval.update(100, 500, 0.0);
637 eval.update(200, 1000, 0.0);
638 assert_eq!(eval.rows_processed(), 300);
639 assert_eq!(eval.bytes_consumed(), 1500);
640 }
641}