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