dataprof_runtime/
hint_binding.rs1use dataprof_core::{SemanticHintBinding, SemanticHintKind, SemanticHints};
4use dataprof_metrics::{is_null_like_token, value_matches_hint};
5
6#[derive(Debug, Clone)]
7struct BindingCounter {
8 binding: SemanticHintBinding,
9}
10
11#[derive(Debug, Clone, Default)]
15pub struct ValueHintBindingAccumulator {
16 counters: Vec<BindingCounter>,
17}
18
19impl ValueHintBindingAccumulator {
20 pub fn new(hints: &SemanticHints) -> Self {
21 let positive = hints.positive_columns.iter().map(|column| BindingCounter {
22 binding: SemanticHintBinding {
23 column: column.clone(),
24 kind: SemanticHintKind::Positive,
25 checked_values: 0,
26 matched_values: 0,
27 exact: true,
28 },
29 });
30 let temporal = hints.temporal_columns.iter().map(|column| BindingCounter {
31 binding: SemanticHintBinding {
32 column: column.clone(),
33 kind: SemanticHintKind::Temporal,
34 checked_values: 0,
35 matched_values: 0,
36 exact: true,
37 },
38 });
39 Self {
40 counters: positive.chain(temporal).collect(),
41 }
42 }
43
44 pub fn observe(&mut self, column: &str, value: &str) {
47 if is_null_like_token(value.trim()) {
48 return;
49 }
50 for counter in self
51 .counters
52 .iter_mut()
53 .filter(|counter| counter.binding.column == column)
54 {
55 counter.binding.checked_values += 1;
56 if value_matches_hint(value, counter.binding.kind) {
57 counter.binding.matched_values += 1;
58 }
59 }
60 }
61
62 pub fn bindings<'a>(
65 &self,
66 column_names: impl IntoIterator<Item = &'a str>,
67 ) -> Vec<SemanticHintBinding> {
68 let names: std::collections::HashSet<&str> = column_names.into_iter().collect();
69 self.counters
70 .iter()
71 .filter(|counter| names.contains(counter.binding.column.as_str()))
72 .map(|counter| counter.binding.clone())
73 .collect()
74 }
75
76 pub fn merge(&mut self, other: &Self) {
77 for other_counter in &other.counters {
78 if let Some(counter) = self.counters.iter_mut().find(|candidate| {
79 candidate.binding.column == other_counter.binding.column
80 && candidate.binding.kind == other_counter.binding.kind
81 }) {
82 counter.binding.checked_values += other_counter.binding.checked_values;
83 counter.binding.matched_values += other_counter.binding.matched_values;
84 } else {
85 self.counters.push(other_counter.clone());
86 }
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn accumulates_exact_counts_with_calculator_semantics() {
97 let hints = SemanticHints::new(vec!["amount".to_string()], vec![])
98 .with_temporal_columns(vec!["event".to_string()]);
99 let mut accumulator = ValueHintBindingAccumulator::new(&hints);
100 for value in ["1", " 2", "NULL"] {
101 accumulator.observe("amount", value);
102 }
103 for value in ["2020-01-01", "not-a-date", " 2021-01-01"] {
104 accumulator.observe("event", value);
105 }
106
107 let bindings = accumulator.bindings(["amount", "event"]);
108 assert_eq!(bindings[0].checked_values, 2);
109 assert_eq!(bindings[0].matched_values, 1);
110 assert!(bindings[0].exact);
111 assert_eq!(bindings[1].checked_values, 3);
112 assert_eq!(bindings[1].matched_values, 1);
113 assert!(bindings[1].exact);
114 }
115
116 #[test]
117 fn omits_unknown_columns() {
118 let hints = SemanticHints::new(vec!["missing".to_string()], vec![]);
119 let accumulator = ValueHintBindingAccumulator::new(&hints);
120 assert!(accumulator.bindings(["present"]).is_empty());
121 }
122}