use sqlparser::ast::{Expr, Ident};
use crate::error::{DiffError, Result};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentCasing {
FoldUnquoted,
FoldUnquotedUpper,
FoldAll,
FoldNone,
}
impl IdentCasing {
pub fn fold(self, id: &Ident) -> String {
match (self, id.quote_style) {
(IdentCasing::FoldAll, _) => id.value.to_ascii_lowercase(),
(IdentCasing::FoldNone, _) => id.value.clone(),
(_, Some(_)) => id.value.clone(),
(IdentCasing::FoldUnquoted, None) => id.value.to_ascii_lowercase(),
(IdentCasing::FoldUnquotedUpper, None) => id.value.to_ascii_uppercase(),
}
}
}
#[derive(Debug, Clone)]
pub struct ColRef {
pub qualifier: Option<Ident>,
pub name: Ident,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Match {
Is,
Not,
Ambiguous,
}
impl ColRef {
pub fn bare(name: impl Into<String>) -> Self {
ColRef {
qualifier: None,
name: Ident::new(name.into()),
}
}
pub fn from_expr(e: &Expr) -> Option<ColRef> {
match e {
Expr::Identifier(id) => Some(ColRef {
qualifier: None,
name: id.clone(),
}),
Expr::CompoundIdentifier(parts) => parts.last().map(|last| {
let qualifier = if parts.len() >= 2 {
Some(parts[parts.len() - 2].clone())
} else {
None
};
ColRef {
qualifier,
name: last.clone(),
}
}),
Expr::Nested(inner) => ColRef::from_expr(inner),
_ => None,
}
}
pub fn from_wrt_arg(func: &str, e: &Expr) -> Result<ColRef> {
ColRef::from_expr(e).ok_or_else(|| {
DiffError::InvalidMarker(format!(
"{func}(): the differentiation variable must be a bare column, but got `{e}`. \
Differentiate with respect to a single column (e.g. `{func}(x * y, x)`), not an \
expression like `x + y`"
))
})
}
pub fn classify(&self, wrt: &ColRef, casing: IdentCasing) -> Match {
if casing.fold(&self.name) != casing.fold(&wrt.name) {
return Match::Not;
}
match (&self.qualifier, &wrt.qualifier) {
(Some(sq), Some(wq)) => {
if casing.fold(sq) == casing.fold(wq) {
Match::Is
} else {
Match::Not
}
}
(None, None) => Match::Is,
(Some(_), None) | (None, Some(_)) => Match::Ambiguous,
}
}
pub fn display(&self) -> String {
match &self.qualifier {
Some(q) => format!("{q}.{}", self.name),
None => self.name.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(s: &str) -> Ident {
Ident::new(s)
}
fn quoted(s: &str) -> Ident {
Ident::with_quote('"', s)
}
#[test]
fn unquoted_folds_case_in_every_dialect() {
assert_eq!(
IdentCasing::FoldUnquoted.fold(&id("Temp")),
IdentCasing::FoldUnquoted.fold(&id("temp"))
);
assert_eq!(
IdentCasing::FoldAll.fold(&id("Temp")),
IdentCasing::FoldAll.fold(&id("temp"))
);
assert_eq!(
IdentCasing::FoldUnquotedUpper.fold(&id("Temp")),
IdentCasing::FoldUnquotedUpper.fold(&id("temp"))
);
}
#[test]
fn an_unquoted_identifier_matches_the_quoting_its_engine_normalizes_to() {
assert_eq!(
IdentCasing::FoldUnquoted.fold(&id("X")),
IdentCasing::FoldUnquoted.fold("ed("x"))
);
assert_ne!(
IdentCasing::FoldUnquoted.fold(&id("X")),
IdentCasing::FoldUnquoted.fold("ed("X"))
);
assert_eq!(
IdentCasing::FoldUnquotedUpper.fold(&id("X")),
IdentCasing::FoldUnquotedUpper.fold("ed("X"))
);
assert_ne!(
IdentCasing::FoldUnquotedUpper.fold(&id("X")),
IdentCasing::FoldUnquotedUpper.fold("ed("x"))
);
}
#[test]
fn quoted_folding_is_per_dialect() {
assert_eq!(
IdentCasing::FoldAll.fold("ed("Temp")),
IdentCasing::FoldAll.fold("ed("temp"))
);
assert_ne!(
IdentCasing::FoldUnquoted.fold("ed("Temp")),
IdentCasing::FoldUnquoted.fold("ed("temp"))
);
}
#[test]
fn bare_wrt_matches_bare_occurrence() {
let x = ColRef::bare("x");
assert_eq!(x.classify(&x, IdentCasing::FoldUnquoted), Match::Is);
assert_eq!(
ColRef::bare("y").classify(&x, IdentCasing::FoldUnquoted),
Match::Not
);
}
#[test]
fn qualified_wrt_disambiguates_across_a_join() {
let ax = ColRef {
qualifier: Some(id("a")),
name: id("x"),
};
let bx = ColRef {
qualifier: Some(id("b")),
name: id("x"),
};
assert_eq!(ax.classify(&ax, IdentCasing::FoldUnquoted), Match::Is);
assert_eq!(bx.classify(&ax, IdentCasing::FoldUnquoted), Match::Not);
}
#[test]
fn bare_occurrence_with_qualified_wrt_is_ambiguous() {
let bare_x = ColRef::bare("x");
let ax = ColRef {
qualifier: Some(id("a")),
name: id("x"),
};
assert_eq!(
bare_x.classify(&ax, IdentCasing::FoldUnquoted),
Match::Ambiguous
);
}
#[test]
fn qualified_occurrence_with_bare_wrt_is_ambiguous() {
let ax = ColRef {
qualifier: Some(id("a")),
name: id("x"),
};
let bare_x = ColRef::bare("x");
assert_eq!(
ax.classify(&bare_x, IdentCasing::FoldUnquoted),
Match::Ambiguous
);
}
}