use serde::{Deserialize, Serialize};
use crate::errors::DataProfilerError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticHintKind {
Positive,
Identifier,
Temporal,
}
impl SemanticHintKind {
pub fn field_name(self) -> &'static str {
match self {
Self::Positive => "positive_columns",
Self::Identifier => "identifier_columns",
Self::Temporal => "temporal_columns",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticHintBinding {
pub column: String,
pub kind: SemanticHintKind,
pub checked_values: usize,
pub matched_values: usize,
pub exact: bool,
}
impl SemanticHintBinding {
pub fn is_proven_inert(&self) -> bool {
self.exact && self.checked_values > 0 && self.matched_values == 0
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SemanticHints {
pub positive_columns: Vec<String>,
pub identifier_columns: Vec<String>,
pub temporal_columns: Vec<String>,
}
impl SemanticHints {
pub fn new(positive_columns: Vec<String>, identifier_columns: Vec<String>) -> Self {
Self {
positive_columns,
identifier_columns,
temporal_columns: Vec::new(),
}
}
pub fn with_temporal_columns(mut self, temporal_columns: Vec<String>) -> Self {
self.temporal_columns = temporal_columns;
self
}
pub fn is_empty(&self) -> bool {
self.positive_columns.is_empty()
&& self.identifier_columns.is_empty()
&& self.temporal_columns.is_empty()
}
pub fn is_identifier_column(&self, column_name: &str) -> bool {
self.identifier_columns
.iter()
.any(|candidate| candidate == column_name)
}
pub fn is_positive_column(&self, column_name: &str) -> bool {
self.positive_columns
.iter()
.any(|candidate| candidate == column_name)
}
pub fn is_temporal_column(&self, column_name: &str) -> bool {
self.temporal_columns
.iter()
.any(|candidate| candidate == column_name)
}
pub fn iter(&self) -> impl Iterator<Item = (SemanticHintKind, &str)> {
let positive = self
.positive_columns
.iter()
.map(|c| (SemanticHintKind::Positive, c.as_str()));
let identifier = self
.identifier_columns
.iter()
.map(|c| (SemanticHintKind::Identifier, c.as_str()));
let temporal = self
.temporal_columns
.iter()
.map(|c| (SemanticHintKind::Temporal, c.as_str()));
positive.chain(identifier).chain(temporal)
}
pub fn validate_names(&self, column_names: &[&str]) -> Result<(), DataProfilerError> {
let mut unknown: Vec<(SemanticHintKind, &str)> = self
.iter()
.filter(|(_, name)| !column_names.contains(name))
.collect();
if unknown.is_empty() {
return Ok(());
}
unknown.sort_unstable_by(|a, b| a.1.cmp(b.1).then(a.0.field_name().cmp(b.0.field_name())));
let details = unknown
.iter()
.map(|(kind, name)| format!("'{}' ({})", name, kind.field_name()))
.collect::<Vec<_>>()
.join(", ");
let mut available: Vec<&str> = column_names.to_vec();
available.sort_unstable();
let available = available
.iter()
.map(|c| format!("'{c}'"))
.collect::<Vec<_>>()
.join(", ");
Err(DataProfilerError::InvalidSemanticHint {
message: format!("semantic hint names not found in the data: {details}"),
suggestion: format!(
"Available columns: [{available}]. Check spelling and case, or remove the hint."
),
})
}
pub fn validate_quality_usage(&self, quality_computed: bool) -> Result<(), DataProfilerError> {
if quality_computed
|| (self.positive_columns.is_empty() && self.temporal_columns.is_empty())
{
return Ok(());
}
Err(DataProfilerError::InvalidSemanticHint {
message: "positive_columns and temporal_columns require the Quality metric pack"
.to_string(),
suggestion: "Include MetricPack::Quality (\"quality\" in Python), or remove the value-driven hint. Identifier hints remain usable without quality because they affect column typing."
.to_string(),
})
}
pub fn validate_bindings(
&self,
bindings: &[SemanticHintBinding],
) -> Result<(), DataProfilerError> {
let inert: Vec<&SemanticHintBinding> =
bindings.iter().filter(|b| b.is_proven_inert()).collect();
if inert.is_empty() {
return Ok(());
}
let details = inert
.iter()
.map(|b| {
format!(
"'{}' ({}): 0 of {} value(s) matched",
b.column,
b.kind.field_name(),
b.checked_values
)
})
.collect::<Vec<_>>()
.join("; ");
Err(DataProfilerError::InvalidSemanticHint {
message: format!("semantic hint(s) bound to no matching value: {details}"),
suggestion: "positive_columns need numeric values; temporal_columns need date values. \
Fix the column choice or remove the hint."
.to_string(),
})
}
}