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    AmbiguousFunction,
21    AmbiguousColumn,
22    TypeMismatch,
23    MissingAnnotation,
24    InvalidAnnotation,
25    ColumnCountMismatch,
26    DuplicateAlias,
27    InvalidRecursion,
28    /// The user's configuration is wrong — an unknown `[[sql.gen]]` key, an
29    /// unrecognized option value, a malformed manifest override. Distinct from
30    /// [`ErrorCode::InternalError`], which means scythe itself is at fault.
31    InvalidConfig,
32    /// A construct the analyzer diagnosed correctly but that has no neutral-type
33    /// representation scythe can hand to codegen: a set-returning function's
34    /// anonymous `record` column referenced in select-list position
35    /// (`json_each`, `json_populate_record`, ...), a multi-element `ROW(...)`/
36    /// tuple constructor, or any other expression `infer_expr_type` genuinely
37    /// cannot resolve. Distinct from [`ErrorCode::TypeMismatch`], which covers
38    /// an expression that *could* have been typed but nothing in the query gave
39    /// it one (a bare NULL/placeholder, both arms of a UNION untyped) — this
40    /// code is for input scythe understood and rejected on purpose (#223),
41    /// mirroring the existing one-code-per-marker-family pattern
42    /// ([`ErrorCode::AmbiguousColumn`], [`ErrorCode::UnknownColumn`],
43    /// [`ErrorCode::UnknownFunction`]) rather than folding into `TypeMismatch`.
44    UnresolvedType,
45    InternalError,
46}
47
48impl fmt::Display for ErrorCode {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            ErrorCode::SyntaxError => write!(f, "SYNTAX_ERROR"),
52            ErrorCode::UnknownTable => write!(f, "UNKNOWN_TABLE"),
53            ErrorCode::UnknownColumn => write!(f, "UNKNOWN_COLUMN"),
54            ErrorCode::UnknownFunction => write!(f, "UNKNOWN_FUNCTION"),
55            ErrorCode::AmbiguousFunction => write!(f, "AMBIGUOUS_FUNCTION"),
56            ErrorCode::AmbiguousColumn => write!(f, "AMBIGUOUS_COLUMN"),
57            ErrorCode::TypeMismatch => write!(f, "TYPE_MISMATCH"),
58            ErrorCode::MissingAnnotation => write!(f, "MISSING_ANNOTATION"),
59            ErrorCode::InvalidAnnotation => write!(f, "INVALID_ANNOTATION"),
60            ErrorCode::ColumnCountMismatch => write!(f, "COLUMN_COUNT_MISMATCH"),
61            ErrorCode::DuplicateAlias => write!(f, "DUPLICATE_ALIAS"),
62            ErrorCode::InvalidRecursion => write!(f, "INVALID_RECURSION"),
63            ErrorCode::InvalidConfig => write!(f, "INVALID_CONFIG"),
64            ErrorCode::UnresolvedType => write!(f, "UNRESOLVED_TYPE"),
65            ErrorCode::InternalError => write!(f, "INTERNAL_ERROR"),
66        }
67    }
68}
69
70impl fmt::Display for ScytheError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}: {}", self.code, self.message)
73    }
74}
75
76impl std::error::Error for ScytheError {}
77
78impl ScytheError {
79    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
80        Self {
81            code,
82            message: message.into(),
83        }
84    }
85
86    pub fn syntax(msg: impl Into<String>) -> Self {
87        Self::new(ErrorCode::SyntaxError, msg)
88    }
89
90    pub fn unknown_table(name: &str) -> Self {
91        Self::new(ErrorCode::UnknownTable, format!("table \"{name}\" does not exist"))
92    }
93
94    pub fn unknown_column(name: &str) -> Self {
95        Self::new(ErrorCode::UnknownColumn, format!("column \"{name}\" does not exist"))
96    }
97
98    pub fn unknown_function(name: &str) -> Self {
99        Self::new(
100            ErrorCode::UnknownFunction,
101            format!("function \"{name}\" does not exist"),
102        )
103    }
104
105    pub fn ambiguous_function(name: &str) -> Self {
106        Self::new(
107            ErrorCode::AmbiguousFunction,
108            format!("function call \"{name}\" is ambiguous"),
109        )
110    }
111
112    pub fn ambiguous_column(name: &str) -> Self {
113        Self::new(ErrorCode::AmbiguousColumn, format!("column \"{name}\" is ambiguous"))
114    }
115
116    pub fn type_mismatch(msg: impl Into<String>) -> Self {
117        Self::new(ErrorCode::TypeMismatch, msg)
118    }
119
120    pub fn missing_annotation(what: &str) -> Self {
121        Self::new(ErrorCode::MissingAnnotation, format!("missing @{what} annotation"))
122    }
123
124    pub fn invalid_annotation(msg: impl Into<String>) -> Self {
125        Self::new(ErrorCode::InvalidAnnotation, msg)
126    }
127
128    pub fn column_count_mismatch(left: usize, right: usize) -> Self {
129        Self::new(
130            ErrorCode::ColumnCountMismatch,
131            format!("column count mismatch: {left} vs {right}"),
132        )
133    }
134
135    pub fn duplicate_alias(name: &str) -> Self {
136        Self::new(ErrorCode::DuplicateAlias, format!("duplicate column alias \"{name}\""))
137    }
138
139    /// The explicit column alias list on a CTE (`WITH t(a, b) AS ...`) has a
140    /// different entry count than the CTE body's output columns. PostgreSQL
141    /// rejects this at parse/plan time, so scythe must too — silently matching
142    /// by position would mislabel columns.
143    pub fn cte_column_alias_mismatch(alias_count: usize, column_count: usize) -> Self {
144        Self::new(
145            ErrorCode::ColumnCountMismatch,
146            format!("CTE column alias list has {alias_count} entries but the CTE body produces {column_count} columns"),
147        )
148    }
149
150    pub fn invalid_recursion(msg: impl Into<String>) -> Self {
151        Self::new(ErrorCode::InvalidRecursion, msg)
152    }
153
154    /// Build an error for user-supplied configuration that scythe understood
155    /// well enough to reject — a misspelled option key, an out-of-range value.
156    ///
157    /// Prefer this over [`ScytheError::new`] with [`ErrorCode::InternalError`]
158    /// anywhere the offending input came from `scythe.toml` rather than from
159    /// scythe's own state.
160    pub fn invalid_config(msg: impl Into<String>) -> Self {
161        Self::new(ErrorCode::InvalidConfig, msg)
162    }
163
164    /// A set-returning function's anonymous `record` column referenced in
165    /// select-list position -- `json_each`/`jsonb_each`/`json_each_text`/
166    /// `jsonb_each_text` and the `json_populate_record`/`jsonb_populate_record`/
167    /// `json_populate_recordset`/`jsonb_populate_recordset` family. PostgreSQL's
168    /// `record` pseudo-type has no neutral-type representation, but every one
169    /// of these functions has a FROM-clause form that expands into real,
170    /// typeable columns instead (#223).
171    ///
172    /// The message deliberately does not hardcode `.key`/`.value` as the
173    /// worked example: that's the right rewrite for the `json_each`/
174    /// `json_each_text` family (see
175    /// `testing_data/types/json_jsonb_advanced/06_jsonb_each_from.json`) but
176    /// wrong for `json_populate_record`, whose columns come from a caller-supplied
177    /// row type instead -- one message covering both families has to stay
178    /// generic about the field names.
179    pub fn untypeable_record(subject: &str, column: &str, function: &str) -> Self {
180        Self::new(
181            ErrorCode::UnresolvedType,
182            format!(
183                "{subject} \"{column}\": {function}(...) in the select list returns PostgreSQL's anonymous \
184                 `record` type, which scythe cannot resolve to a type it can generate code for -- move it \
185                 into the FROM clause instead (e.g. `FROM ..., {function}(...) AS {column}`) and select \
186                 {column}'s fields directly instead of the whole record"
187            ),
188        )
189    }
190
191    /// A multi-element `ROW(...)`/tuple constructor -- `(a, b)` (parses as
192    /// `Expr::Tuple`) or the explicit `ROW(a, b)` call (parses as a function
193    /// named `row`) -- neither of which has a single neutral type (#117, #223).
194    pub fn untypeable_row_constructor(subject: &str, column: &str) -> Self {
195        Self::new(
196            ErrorCode::UnresolvedType,
197            format!(
198                "{subject} \"{column}\": ROW(...) builds a row with no single neutral type scythe can name -- \
199                 CAST it to a named composite type, or select its fields individually instead of one ROW(...) value"
200            ),
201        )
202    }
203
204    /// A generic fallback for any other expression `infer_expr_type` cannot
205    /// resolve to a type: an unresolved composite field access, `unnest` over
206    /// a non-array argument, a subquery producing no columns, a table-valued
207    /// function scythe has no column-type mapping for, and the handful of AST
208    /// shapes with no dedicated inference arm (#223).
209    pub fn unresolved_expression_type(subject: &str, column: &str) -> Self {
210        Self::new(
211            ErrorCode::UnresolvedType,
212            format!("{subject} \"{column}\": scythe could not determine a type for this expression"),
213        )
214    }
215}