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 counters = hints
22 .iter()
23 .filter(|(kind, _)| *kind != SemanticHintKind::Identifier)
24 .map(|(kind, column)| BindingCounter {
25 binding: SemanticHintBinding {
26 column: column.to_string(),
27 kind,
28 checked_values: 0,
29 matched_values: 0,
30 exact: true,
31 },
32 });
33 Self {
34 counters: counters.collect(),
35 }
36 }
37
38 pub fn observe(&mut self, column: &str, value: &str) {
41 if is_null_like_token(value.trim()) {
42 return;
43 }
44 for counter in self
45 .counters
46 .iter_mut()
47 .filter(|counter| counter.binding.column == column)
48 {
49 counter.binding.checked_values += 1;
50 if value_matches_hint(value, counter.binding.kind) {
51 counter.binding.matched_values += 1;
52 }
53 }
54 }
55
56 pub fn bindings<'a>(
59 &self,
60 column_names: impl IntoIterator<Item = &'a str>,
61 ) -> Vec<SemanticHintBinding> {
62 let names: std::collections::HashSet<&str> = column_names.into_iter().collect();
63 self.counters
64 .iter()
65 .filter(|counter| names.contains(counter.binding.column.as_str()))
66 .map(|counter| counter.binding.clone())
67 .collect()
68 }
69
70 pub fn merge(&mut self, other: &Self) {
71 for other_counter in &other.counters {
72 if let Some(counter) = self.counters.iter_mut().find(|candidate| {
73 candidate.binding.column == other_counter.binding.column
74 && candidate.binding.kind == other_counter.binding.kind
75 }) {
76 counter.binding.checked_values += other_counter.binding.checked_values;
77 counter.binding.matched_values += other_counter.binding.matched_values;
78 } else {
79 self.counters.push(other_counter.clone());
80 }
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn accumulates_exact_counts_with_calculator_semantics() {
91 let hints = SemanticHints::new(vec!["amount".to_string()], vec![])
92 .with_temporal_columns(vec!["event".to_string()]);
93 let mut accumulator = ValueHintBindingAccumulator::new(&hints);
94 for value in ["1", " 2", " -inf ", "NULL"] {
95 accumulator.observe("amount", value);
96 }
97 for value in ["2020-01-01", "not-a-date", " 2021-01-01"] {
98 accumulator.observe("event", value);
99 }
100
101 let bindings = accumulator.bindings(["amount", "event"]);
102 assert_eq!(bindings[0].checked_values, 3);
103 assert_eq!(bindings[0].matched_values, 2);
104 assert!(bindings[0].exact);
105 assert_eq!(bindings[1].checked_values, 3);
106 assert_eq!(bindings[1].matched_values, 1);
107 assert!(bindings[1].exact);
108 }
109
110 #[test]
111 fn omits_unknown_columns() {
112 let hints = SemanticHints::new(vec!["missing".to_string()], vec![]);
113 let accumulator = ValueHintBindingAccumulator::new(&hints);
114 assert!(accumulator.bindings(["present"]).is_empty());
115 }
116}