use dataflow_rs::engine::error::DataflowError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryError {
UnsupportedInQuery { op: String, at: String },
NotRepresentable { what: String, at: String },
InvalidEnvelope(String),
InvalidField {
field: String,
at: String,
did_you_mean: Option<String>,
},
MissingParam { name: String, at: String },
UnknownRelation { relation: String, at: String },
UndeclaredEntity {
entity: String,
did_you_mean: Option<String>,
},
NoQueryableColumns { entity: String },
EntityNotAllowed { entity: String, physical: String },
LimitExceeded { requested: u64, max: u64 },
SkipExceeded { requested: u64, max: u64 },
FeatureUnsupportedByTarget { feature: String, target: String },
}
impl std::fmt::Display for QueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QueryError::UnsupportedInQuery { op, at } => {
write!(f, "operator '{op}' is not supported in a query (at {at})")
}
QueryError::NotRepresentable { what, at } => {
write!(f, "{what} has no portable form in v1 (at {at})")
}
QueryError::InvalidEnvelope(msg) => write!(f, "invalid envelope: {msg}"),
QueryError::InvalidField {
field,
at,
did_you_mean,
} => {
write!(f, "invalid field reference '{field}' (at {at})")?;
write_suggestion(f, did_you_mean)
}
QueryError::MissingParam { name, at } => {
write!(f, "query references undefined param '{name}' (at {at})")
}
QueryError::UnknownRelation { relation, at } => {
write!(f, "unknown relation '{relation}' (at {at})")
}
QueryError::UndeclaredEntity {
entity,
did_you_mean,
} => {
write!(
f,
"entity '{entity}' is not declared in the task's schema: add \
\"schema\": {{\"entities\": {{\"{entity}\": {{\"columns\": \
{{\"<column>\": {{}}}}}}}}}} naming the columns this task uses, \
or add \"unmapped\": \"identity\" to that schema to accept \
undeclared names as physical ones (pre-1.0 behaviour)"
)?;
write_suggestion(f, did_you_mean)
}
QueryError::NoQueryableColumns { entity } => write!(
f,
"entity '{entity}' declares columns but none of them are queryable, \
so a query naming no \"fields\" has nothing it may return: mark a \
column \"queryable\": true, or read it through a different entity"
),
QueryError::EntityNotAllowed { entity, physical } => write!(
f,
"entity '{entity}' resolves to '{physical}', which the connector's \
allowed_entities list does not permit"
),
QueryError::FeatureUnsupportedByTarget { feature, target } => {
write!(f, "{feature} is not supported by the {target} backend")
}
QueryError::LimitExceeded { requested, max } => {
write!(
f,
"requested limit {requested} exceeds the configured maximum {max}"
)
}
QueryError::SkipExceeded { requested, max } => {
write!(
f,
"requested skip {requested} exceeds the configured maximum {max}"
)
}
}
}
}
impl std::error::Error for QueryError {}
impl QueryError {
pub fn is_connector_detail(&self) -> bool {
matches!(self, QueryError::EntityNotAllowed { .. })
}
}
impl From<QueryError> for DataflowError {
fn from(e: QueryError) -> Self {
if e.is_connector_detail() {
return crate::errors::connector_detail_error(e);
}
DataflowError::Validation(e.to_string())
}
}
fn write_suggestion(
f: &mut std::fmt::Formatter<'_>,
did_you_mean: &Option<String>,
) -> std::fmt::Result {
match did_you_mean {
Some(name) => write!(f, " — did you mean \"{name}\"?"),
None => Ok(()),
}
}
fn max_distance(candidate: &str) -> usize {
(candidate.chars().count() / 3).clamp(1, 3)
}
pub(crate) fn nearest<'a, I>(name: &str, candidates: I) -> Option<String>
where
I: IntoIterator<Item = &'a str>,
{
let needle: Vec<char> = name.to_lowercase().chars().collect();
candidates
.into_iter()
.filter(|c| *c != name)
.map(|candidate| {
let lowered: Vec<char> = candidate.to_lowercase().chars().collect();
(
crate::text::edit_distance_chars(&needle, &lowered),
candidate,
)
})
.filter(|(distance, candidate)| *distance <= max_distance(candidate))
.min_by(|a, b| a.0.cmp(&b.0))
.map(|(_, candidate)| candidate.to_string())
}
#[cfg(test)]
mod suggestion_tests {
use super::*;
#[test]
fn a_near_miss_is_offered() {
assert_eq!(
nearest("fileds", ["source", "filter", "fields", "sort"]),
Some("fields".to_string())
);
assert_eq!(
nearest("Name", ["id", "name", "email"]),
Some("name".to_string())
);
}
#[test]
fn an_unrelated_name_is_not_offered() {
assert_eq!(nearest("id", ["age", "name", "email"]), None);
assert_eq!(nearest("customer_reference", ["id", "name"]), None);
assert_eq!(nearest("", ["id"]), None);
}
#[test]
fn the_name_itself_is_never_offered() {
assert_eq!(nearest("secret", ["secret"]), None);
assert_eq!(
nearest("secret", ["secret", "secrets"]),
Some("secrets".to_string())
);
}
}