Skip to main content

scythe_core/
errors.rs

1use std::fmt;
2
3#[derive(Debug)]
4pub struct ScytheError {
5    pub code: ErrorCode,
6    pub message: String,
7}
8
9/// A machine-readable classification for a [`ScytheError`].
10///
11/// Marked `#[non_exhaustive]` so that adding a variant is not a breaking change
12/// for downstream crates matching on it.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum ErrorCode {
16    SyntaxError,
17    UnknownTable,
18    UnknownColumn,
19    UnknownFunction,
20    AmbiguousColumn,
21    TypeMismatch,
22    MissingAnnotation,
23    InvalidAnnotation,
24    ColumnCountMismatch,
25    DuplicateAlias,
26    InvalidRecursion,
27    /// The user's configuration is wrong — an unknown `[[sql.gen]]` key, an
28    /// unrecognized option value, a malformed manifest override. Distinct from
29    /// [`ErrorCode::InternalError`], which means scythe itself is at fault.
30    InvalidConfig,
31    InternalError,
32}
33
34impl fmt::Display for ErrorCode {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            ErrorCode::SyntaxError => write!(f, "SYNTAX_ERROR"),
38            ErrorCode::UnknownTable => write!(f, "UNKNOWN_TABLE"),
39            ErrorCode::UnknownColumn => write!(f, "UNKNOWN_COLUMN"),
40            ErrorCode::UnknownFunction => write!(f, "UNKNOWN_FUNCTION"),
41            ErrorCode::AmbiguousColumn => write!(f, "AMBIGUOUS_COLUMN"),
42            ErrorCode::TypeMismatch => write!(f, "TYPE_MISMATCH"),
43            ErrorCode::MissingAnnotation => write!(f, "MISSING_ANNOTATION"),
44            ErrorCode::InvalidAnnotation => write!(f, "INVALID_ANNOTATION"),
45            ErrorCode::ColumnCountMismatch => write!(f, "COLUMN_COUNT_MISMATCH"),
46            ErrorCode::DuplicateAlias => write!(f, "DUPLICATE_ALIAS"),
47            ErrorCode::InvalidRecursion => write!(f, "INVALID_RECURSION"),
48            ErrorCode::InvalidConfig => write!(f, "INVALID_CONFIG"),
49            ErrorCode::InternalError => write!(f, "INTERNAL_ERROR"),
50        }
51    }
52}
53
54impl fmt::Display for ScytheError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "{}: {}", self.code, self.message)
57    }
58}
59
60impl std::error::Error for ScytheError {}
61
62impl ScytheError {
63    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
64        Self {
65            code,
66            message: message.into(),
67        }
68    }
69
70    pub fn syntax(msg: impl Into<String>) -> Self {
71        Self::new(ErrorCode::SyntaxError, msg)
72    }
73
74    pub fn unknown_table(name: &str) -> Self {
75        Self::new(ErrorCode::UnknownTable, format!("table \"{name}\" does not exist"))
76    }
77
78    pub fn unknown_column(name: &str) -> Self {
79        Self::new(ErrorCode::UnknownColumn, format!("column \"{name}\" does not exist"))
80    }
81
82    pub fn unknown_function(name: &str) -> Self {
83        Self::new(
84            ErrorCode::UnknownFunction,
85            format!("function \"{name}\" does not exist"),
86        )
87    }
88
89    pub fn ambiguous_column(name: &str) -> Self {
90        Self::new(ErrorCode::AmbiguousColumn, format!("column \"{name}\" is ambiguous"))
91    }
92
93    pub fn type_mismatch(msg: impl Into<String>) -> Self {
94        Self::new(ErrorCode::TypeMismatch, msg)
95    }
96
97    pub fn missing_annotation(what: &str) -> Self {
98        Self::new(ErrorCode::MissingAnnotation, format!("missing @{what} annotation"))
99    }
100
101    pub fn invalid_annotation(msg: impl Into<String>) -> Self {
102        Self::new(ErrorCode::InvalidAnnotation, msg)
103    }
104
105    pub fn column_count_mismatch(left: usize, right: usize) -> Self {
106        Self::new(
107            ErrorCode::ColumnCountMismatch,
108            format!("column count mismatch: {left} vs {right}"),
109        )
110    }
111
112    pub fn duplicate_alias(name: &str) -> Self {
113        Self::new(ErrorCode::DuplicateAlias, format!("duplicate column alias \"{name}\""))
114    }
115
116    /// The explicit column alias list on a CTE (`WITH t(a, b) AS ...`) has a
117    /// different entry count than the CTE body's output columns. PostgreSQL
118    /// rejects this at parse/plan time, so scythe must too — silently matching
119    /// by position would mislabel columns.
120    pub fn cte_column_alias_mismatch(alias_count: usize, column_count: usize) -> Self {
121        Self::new(
122            ErrorCode::ColumnCountMismatch,
123            format!("CTE column alias list has {alias_count} entries but the CTE body produces {column_count} columns"),
124        )
125    }
126
127    pub fn invalid_recursion(msg: impl Into<String>) -> Self {
128        Self::new(ErrorCode::InvalidRecursion, msg)
129    }
130
131    /// Build an error for user-supplied configuration that scythe understood
132    /// well enough to reject — a misspelled option key, an out-of-range value.
133    ///
134    /// Prefer this over [`ScytheError::new`] with [`ErrorCode::InternalError`]
135    /// anywhere the offending input came from `scythe.toml` rather than from
136    /// scythe's own state.
137    pub fn invalid_config(msg: impl Into<String>) -> Self {
138        Self::new(ErrorCode::InvalidConfig, msg)
139    }
140}