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