[go: up one dir, main page]

Skip to main content

sql_docs/
error.rs

1//! Error types returned by this crate’s public APIs.
2
3use crate::{
4    comments::CommentError,
5    docs::{ColumnDoc, TableDoc},
6};
7use core::fmt;
8use sqlparser::parser::ParserError;
9use std::{error, fmt::Debug};
10
11/// Errors that can occur while discovering, parsing, or documenting SQL files.
12#[non_exhaustive]
13#[derive(Debug)]
14pub enum DocError {
15    /// Wrapper for standard [`std::io::Error`]
16    FileReadError(std::io::Error),
17    /// Wrapper for [`CommentError`]
18    CommentError(CommentError),
19    /// Wrapper for [`ParserError`]
20    SqlParserError(ParserError),
21    /// Indicates an invalid or unexpected object name in the [`sqlparser::ast::Statement`]
22    InvalidObjectName {
23        /// The message to accompany the invalid [`sqlparser::ast::ObjectName`]
24        message: String,
25        /// The line number for the invalid [`sqlparser::ast::ObjectName`]
26        line: u64,
27        /// The column number for the invalid [`sqlparser::ast::ObjectName`]
28        column: u64,
29    },
30    /// Table not found when searching [`crate::SqlDoc`]
31    TableNotFound {
32        /// The name of the table that was not found
33        name: String,
34    },
35    /// Column not found when searching [`crate::docs::TableDoc`]
36    ColumnNotFound {
37        /// The name of the column not found
38        name: String,
39    },
40    /// Duplicate tables with same name were found when searching [`crate::SqlDoc`]
41    DuplicateTablesFound {
42        /// `Vec` of the [`TableDoc`] for each duplicate table found
43        tables: Vec<TableDoc>,
44    },
45    /// Duplicate columns with same name were found when searching a single [`TableDoc`]
46    DuplicateColumnsFound {
47        /// `Vec` of the [`crate::docs::ColumnDoc`] for each duplicate table found
48        columns: Vec<ColumnDoc>,
49    },
50    /// Could not find table with `schema`
51    TableWithSchemaNotFound {
52        /// the name of the table not found
53        name: String,
54        /// the schema for the table not found
55        schema: String,
56    },
57}
58
59impl fmt::Display for DocError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::FileReadError(error) => write!(f, "file read error: {error}"),
63            Self::CommentError(comment_error) => {
64                write!(f, "comment parse error: {comment_error}")
65            }
66            Self::SqlParserError(parser_error) => write!(f, "SQL parse error {parser_error}"),
67            Self::InvalidObjectName { message, line, column } => {
68                write!(f, "{message} at line {line}, column {column}")
69            }
70            Self::TableNotFound { name } => write!(f, "Table not found in SqlDoc: {name}"),
71            Self::ColumnNotFound { name } => write!(f, "Column not found in TableDoc: {name}"),
72            Self::DuplicateTablesFound { tables } => {
73                writeln!(f, "Duplicate tables found:")?;
74                for t in tables {
75                    writeln!(f, "{t}")?;
76                }
77                Ok(())
78            }
79            Self::DuplicateColumnsFound { columns } => {
80                writeln!(f, "Duplicate columns found:")?;
81                for t in columns {
82                    writeln!(f, "{t}")?;
83                }
84                Ok(())
85            }
86            Self::TableWithSchemaNotFound { name, schema } => {
87                writeln!(f, "Table: {name} with schema: {schema} not found in SqlDoc")
88            }
89        }
90    }
91}
92
93impl error::Error for DocError {
94    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
95        match self {
96            Self::FileReadError(e) => Some(e),
97            Self::CommentError(e) => Some(e),
98            Self::SqlParserError(e) => Some(e),
99            Self::InvalidObjectName { .. }
100            | Self::TableNotFound { .. }
101            | Self::ColumnNotFound { .. }
102            | Self::DuplicateTablesFound { .. }
103            | Self::DuplicateColumnsFound { .. }
104            | Self::TableWithSchemaNotFound { .. } => None,
105        }
106    }
107}
108
109impl From<std::io::Error> for DocError {
110    fn from(e: std::io::Error) -> Self {
111        Self::FileReadError(e)
112    }
113}
114
115impl From<CommentError> for DocError {
116    fn from(e: CommentError) -> Self {
117        Self::CommentError(e)
118    }
119}
120
121impl From<ParserError> for DocError {
122    fn from(e: ParserError) -> Self {
123        Self::SqlParserError(e)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use sqlparser::parser::ParserError;
130
131    use crate::{comments::CommentError, docs::TableDoc, error::DocError};
132
133    #[test]
134    fn test_doc_errors() {
135        use std::fs;
136
137        use crate::comments::Location;
138        let Err(io_error) = fs::read_dir("INVALID") else {
139            panic!("there should not be a directory called INVALID")
140        };
141        let io_error_str = io_error.to_string();
142        let read_error = DocError::FileReadError(io_error);
143        let expected_read_error = "file read error: ".to_owned() + &io_error_str;
144        assert!(read_error.to_string().contains(&expected_read_error));
145
146        let comment_error = DocError::CommentError(CommentError::UnmatchedMultilineCommentStart {
147            location: Location::default(),
148        });
149        let expected_comment_error =
150            "comment parse error: unmatched block comment start at line 1, column 1";
151        assert_eq!(comment_error.to_string(), expected_comment_error);
152
153        let sql_error = DocError::SqlParserError(ParserError::RecursionLimitExceeded);
154        let expected_sql_error = "SQL parse error sql parser error: recursion limit exceeded";
155        assert_eq!(sql_error.to_string(), expected_sql_error);
156    }
157
158    #[test]
159    fn test_doc_errors_from() {
160        use crate::comments::Location;
161        use std::fs;
162
163        let Err(io_error) = fs::read_dir("INVALID") else {
164            panic!("there should not be a directory called INVALID")
165        };
166        let io_kind = io_error.kind();
167        let doc_io_error: DocError = io_error.into();
168        assert!(matches!(doc_io_error, DocError::FileReadError(_)));
169        if let DocError::FileReadError(inner) = doc_io_error {
170            assert_eq!(inner.kind(), io_kind);
171        }
172
173        let comment_error =
174            CommentError::UnmatchedMultilineCommentStart { location: Location::default() };
175        let comment_error_str = comment_error.to_string();
176        let doc_comment_error: DocError = comment_error.into();
177        assert!(matches!(doc_comment_error, DocError::CommentError(_)));
178        if let DocError::CommentError(inner) = doc_comment_error {
179            assert_eq!(inner.to_string(), comment_error_str);
180        }
181
182        let parser_error = ParserError::RecursionLimitExceeded;
183        let parser_error_str = parser_error.to_string();
184        let doc_parser_error: DocError = parser_error.into();
185        assert!(matches!(doc_parser_error, DocError::SqlParserError(_)));
186        if let DocError::SqlParserError(inner) = doc_parser_error {
187            assert_eq!(inner.to_string(), parser_error_str);
188        }
189    }
190
191    #[test]
192    fn test_doc_error_source() {
193        use std::{error::Error, fs};
194
195        use crate::comments::Location;
196
197        let Err(io_err) = fs::read_dir("INVALID") else {
198            panic!("there should not be a directory called INVALID")
199        };
200        let io_err_str = io_err.to_string();
201        let doc_io = DocError::FileReadError(io_err);
202        let src =
203            doc_io.source().unwrap_or_else(|| panic!("expected Some(source) for FileReadError"));
204        assert_eq!(src.to_string(), io_err_str);
205
206        let comment =
207            CommentError::UnmatchedMultilineCommentStart { location: Location::default() };
208        let comment_str = comment.to_string();
209        let doc_comment = DocError::CommentError(comment);
210        let src = doc_comment
211            .source()
212            .unwrap_or_else(|| panic!("expected Some(source) for CommentError"));
213        assert_eq!(src.to_string(), comment_str);
214
215        let parser = ParserError::RecursionLimitExceeded;
216        let parser_str = parser.to_string();
217        let doc_parser = DocError::SqlParserError(parser);
218        let src = doc_parser
219            .source()
220            .unwrap_or_else(|| panic!("expected Some(source) for SqlParserError"));
221        assert_eq!(src.to_string(), parser_str);
222    }
223
224    fn table_doc_for_test(name: &str) -> TableDoc {
225        TableDoc::new(None, name.to_string(), None, vec![], None)
226    }
227    #[test]
228    fn test_doc_error_display_invalid_object_name() {
229        let e =
230            DocError::InvalidObjectName { message: "bad object".to_string(), line: 12, column: 34 };
231        assert_eq!(e.to_string(), "bad object at line 12, column 34");
232    }
233
234    #[test]
235    fn test_doc_error_display_table_not_found() {
236        let e = DocError::TableNotFound { name: "users".to_string() };
237        assert_eq!(e.to_string(), "Table not found in SqlDoc: users");
238    }
239
240    #[test]
241    fn test_doc_error_display_duplicate_tables_found() {
242        let t1 = table_doc_for_test("dup_table");
243        let t2 = table_doc_for_test("dup_table");
244        let e = DocError::DuplicateTablesFound { tables: vec![t1, t2] };
245        let s = e.to_string();
246        assert!(s.contains("Duplicate tables found:"));
247        assert!(s.contains("dup_table"), "output was: {s}");
248    }
249
250    #[test]
251    fn test_doc_error_source_none_for_non_wrapped_variants() {
252        use std::error::Error as _;
253        let invalid = DocError::InvalidObjectName { message: "x".to_string(), line: 1, column: 1 };
254        assert!(invalid.source().is_none());
255        let not_found = DocError::TableNotFound { name: "x".to_string() };
256        assert!(not_found.source().is_none());
257        let dup = DocError::DuplicateTablesFound { tables: vec![] };
258        assert!(dup.source().is_none());
259    }
260
261    #[test]
262    fn test_doc_error_display_column_not_found() {
263        let e = DocError::ColumnNotFound { name: "id".to_string() };
264        assert_eq!(e.to_string(), "Column not found in TableDoc: id");
265    }
266
267    #[test]
268    fn test_doc_error_display_duplicate_columns_found() {
269        use crate::docs::ColumnDoc;
270
271        let c1 = ColumnDoc::new("dup_col".to_string(), None);
272        let c2 = ColumnDoc::new("dup_col".to_string(), None);
273        let e = DocError::DuplicateColumnsFound { columns: vec![c1, c2] };
274
275        let s = e.to_string();
276        assert!(s.contains("Duplicate columns found:"), "output was: {s}");
277        assert!(s.contains("dup_col"), "output was: {s}");
278    }
279
280    #[test]
281    fn test_doc_error_source_none_for_column_variants() {
282        use std::error::Error as _;
283
284        let not_found = DocError::ColumnNotFound { name: "x".to_string() };
285        assert!(not_found.source().is_none());
286
287        let dup = DocError::DuplicateColumnsFound { columns: vec![] };
288        assert!(dup.source().is_none());
289    }
290
291    #[test]
292    fn test_doc_error_display_table_with_schema_not_found() {
293        let e = DocError::TableWithSchemaNotFound {
294            name: "events".to_string(),
295            schema: "analytics".to_string(),
296        };
297        assert_eq!(e.to_string(), "Table: events with schema: analytics not found in SqlDoc\n");
298    }
299
300    #[test]
301    fn test_doc_error_source_none_for_table_with_schema_not_found() {
302        use std::error::Error as _;
303        let e = DocError::TableWithSchemaNotFound {
304            name: "events".to_string(),
305            schema: "analytics".to_string(),
306        };
307        assert!(e.source().is_none());
308    }
309}