dataprof_core/
semantic.rs1use serde::{Deserialize, Serialize};
2
3use crate::errors::DataProfilerError;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum SemanticHintKind {
9 Positive,
11 Identifier,
13 Temporal,
15}
16
17impl SemanticHintKind {
18 pub fn field_name(self) -> &'static str {
20 match self {
21 Self::Positive => "positive_columns",
22 Self::Identifier => "identifier_columns",
23 Self::Temporal => "temporal_columns",
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct SemanticHintBinding {
41 pub column: String,
43 pub kind: SemanticHintKind,
45 pub checked_values: usize,
47 pub matched_values: usize,
49 pub exact: bool,
51}
52
53impl SemanticHintBinding {
54 pub fn is_proven_inert(&self) -> bool {
58 self.exact && self.checked_values > 0 && self.matched_values == 0
59 }
60}
61
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct SemanticHints {
66 pub positive_columns: Vec<String>,
67 pub identifier_columns: Vec<String>,
68 pub temporal_columns: Vec<String>,
69}
70
71impl SemanticHints {
72 pub fn new(positive_columns: Vec<String>, identifier_columns: Vec<String>) -> Self {
73 Self {
74 positive_columns,
75 identifier_columns,
76 temporal_columns: Vec::new(),
77 }
78 }
79
80 pub fn with_temporal_columns(mut self, temporal_columns: Vec<String>) -> Self {
81 self.temporal_columns = temporal_columns;
82 self
83 }
84
85 pub fn is_empty(&self) -> bool {
86 self.positive_columns.is_empty()
87 && self.identifier_columns.is_empty()
88 && self.temporal_columns.is_empty()
89 }
90
91 pub fn is_identifier_column(&self, column_name: &str) -> bool {
92 self.identifier_columns
93 .iter()
94 .any(|candidate| candidate == column_name)
95 }
96
97 pub fn is_positive_column(&self, column_name: &str) -> bool {
98 self.positive_columns
99 .iter()
100 .any(|candidate| candidate == column_name)
101 }
102
103 pub fn is_temporal_column(&self, column_name: &str) -> bool {
104 self.temporal_columns
105 .iter()
106 .any(|candidate| candidate == column_name)
107 }
108
109 pub fn iter(&self) -> impl Iterator<Item = (SemanticHintKind, &str)> {
111 let positive = self
112 .positive_columns
113 .iter()
114 .map(|c| (SemanticHintKind::Positive, c.as_str()));
115 let identifier = self
116 .identifier_columns
117 .iter()
118 .map(|c| (SemanticHintKind::Identifier, c.as_str()));
119 let temporal = self
120 .temporal_columns
121 .iter()
122 .map(|c| (SemanticHintKind::Temporal, c.as_str()));
123 positive.chain(identifier).chain(temporal)
124 }
125
126 pub fn validate_names(&self, column_names: &[&str]) -> Result<(), DataProfilerError> {
132 let mut unknown: Vec<(SemanticHintKind, &str)> = self
133 .iter()
134 .filter(|(_, name)| !column_names.contains(name))
135 .collect();
136 if unknown.is_empty() {
137 return Ok(());
138 }
139 unknown.sort_unstable_by(|a, b| a.1.cmp(b.1).then(a.0.field_name().cmp(b.0.field_name())));
141
142 let details = unknown
143 .iter()
144 .map(|(kind, name)| format!("'{}' ({})", name, kind.field_name()))
145 .collect::<Vec<_>>()
146 .join(", ");
147 let mut available: Vec<&str> = column_names.to_vec();
148 available.sort_unstable();
149 let available = available
150 .iter()
151 .map(|c| format!("'{c}'"))
152 .collect::<Vec<_>>()
153 .join(", ");
154
155 Err(DataProfilerError::InvalidSemanticHint {
156 message: format!("semantic hint names not found in the data: {details}"),
157 suggestion: format!(
158 "Available columns: [{available}]. Check spelling and case, or remove the hint."
159 ),
160 })
161 }
162
163 pub fn validate_quality_usage(&self, quality_computed: bool) -> Result<(), DataProfilerError> {
169 if quality_computed
170 || (self.positive_columns.is_empty() && self.temporal_columns.is_empty())
171 {
172 return Ok(());
173 }
174
175 Err(DataProfilerError::InvalidSemanticHint {
176 message: "positive_columns and temporal_columns require the Quality metric pack"
177 .to_string(),
178 suggestion: "Include MetricPack::Quality (\"quality\" in Python), or remove the value-driven hint. Identifier hints remain usable without quality because they affect column typing."
179 .to_string(),
180 })
181 }
182
183 pub fn validate_bindings(
189 &self,
190 bindings: &[SemanticHintBinding],
191 ) -> Result<(), DataProfilerError> {
192 let inert: Vec<&SemanticHintBinding> =
193 bindings.iter().filter(|b| b.is_proven_inert()).collect();
194 if inert.is_empty() {
195 return Ok(());
196 }
197
198 let details = inert
199 .iter()
200 .map(|b| {
201 format!(
202 "'{}' ({}): 0 of {} value(s) matched",
203 b.column,
204 b.kind.field_name(),
205 b.checked_values
206 )
207 })
208 .collect::<Vec<_>>()
209 .join("; ");
210
211 Err(DataProfilerError::InvalidSemanticHint {
212 message: format!("semantic hint(s) bound to no matching value: {details}"),
213 suggestion: "positive_columns need numeric values; temporal_columns need date values. \
214 Fix the column choice or remove the hint."
215 .to_string(),
216 })
217 }
218}