1use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
2use tokio_postgres::error::{DbError, Error};
3
4use crate::pool::PgError;
5
6#[derive(Debug, thiserror::Error)]
8pub struct PgDatabaseError {
9 message: String,
10 hint: Option<String>,
11 source_code: Option<NamedSource<String>>,
12 label: Option<miette::SourceSpan>,
13 label_text: String,
14 severity: String,
15 code: String,
16 detail: Option<String>,
17 where_clause: Option<String>,
18 schema: Option<String>,
19 table: Option<String>,
20 column: Option<String>,
21 datatype: Option<String>,
22 constraint: Option<String>,
23 file: Option<String>,
24 line: Option<u32>,
25 routine: Option<String>,
26}
27
28impl std::fmt::Display for PgDatabaseError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 writeln!(f, "{}: {}", self.severity, self.message)?;
31 writeln!(f, " Code: {}", self.code)?;
32
33 if let Some(detail) = &self.detail {
34 writeln!(f, " Detail: {}", detail)?;
35 }
36 if let Some(where_clause) = &self.where_clause {
37 writeln!(f, " Where: {}", where_clause)?;
38 }
39 if let Some(schema) = &self.schema {
40 writeln!(f, " Schema: {}", schema)?;
41 }
42 if let Some(table) = &self.table {
43 writeln!(f, " Table: {}", table)?;
44 }
45 if let Some(column) = &self.column {
46 writeln!(f, " Column: {}", column)?;
47 }
48 if let Some(datatype) = &self.datatype {
49 writeln!(f, " Datatype: {}", datatype)?;
50 }
51 if let Some(constraint) = &self.constraint {
52 writeln!(f, " Constraint: {}", constraint)?;
53 }
54 if let Some(hint) = &self.hint {
55 writeln!(f, " Hint: {}", hint)?;
56 }
57 if let Some(file) = &self.file {
58 write!(f, " Source: {}", file)?;
59 if let Some(line) = self.line {
60 write!(f, ":{}", line)?;
61 }
62 if let Some(routine) = &self.routine {
63 write!(f, " in {}", routine)?;
64 }
65 writeln!(f)?;
66 }
67
68 Ok(())
69 }
70}
71
72impl Diagnostic for PgDatabaseError {
73 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
74 self.hint
75 .as_ref()
76 .map(|h| Box::new(h.clone()) as Box<dyn std::fmt::Display>)
77 }
78
79 fn source_code(&self) -> Option<&dyn SourceCode> {
80 self.source_code.as_ref().map(|s| s as &dyn SourceCode)
81 }
82
83 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
84 if let Some(span) = self.label {
85 Some(Box::new(std::iter::once(LabeledSpan::new_with_span(
86 Some(self.label_text.clone()),
87 span,
88 ))))
89 } else {
90 None
91 }
92 }
93}
94
95impl PgDatabaseError {
96 pub fn from_db_error(db_error: &DbError, query: Option<&str>) -> Self {
98 let message = db_error.message().to_string();
99 let hint = db_error.hint().map(|s| s.to_string());
100 let detail = db_error.detail().map(|s| s.to_string());
101 let code = db_error.code().code().to_string();
102 let severity = db_error
103 .parsed_severity()
104 .map(|s| format!("{:?}", s))
105 .unwrap_or_else(|| db_error.severity().to_string());
106 let position = db_error.position().map(|pos| match pos {
107 tokio_postgres::error::ErrorPosition::Original(p) => format!("position {}", p),
108 tokio_postgres::error::ErrorPosition::Internal { position, query } => {
109 format!("internal position {} in query: {}", position, query)
110 }
111 });
112 let where_clause = db_error.where_().map(|s| s.to_string());
113 let schema = db_error.schema().map(|s| s.to_string());
114 let table = db_error.table().map(|s| s.to_string());
115 let column = db_error.column().map(|s| s.to_string());
116 let datatype = db_error.datatype().map(|s| s.to_string());
117 let constraint = db_error.constraint().map(|s| s.to_string());
118 let file = db_error.file().map(|s| s.to_string());
119 let line = db_error.line();
120 let routine = db_error.routine().map(|s| s.to_string());
121
122 if let (Some(query_str), Some(pos_str)) = (query, &position)
124 && let Some(pos_str) = pos_str.strip_prefix("position ")
125 && let Ok(pos) = pos_str.parse::<usize>()
126 {
127 let pos_zero_based = pos.saturating_sub(1);
129
130 let actual_pos = pos_zero_based.min(query_str.len().saturating_sub(1));
132
133 let source = NamedSource::new("query", query_str.to_string());
134 let span = miette::SourceSpan::from(actual_pos..actual_pos + 1);
135 return Self {
136 message,
137 hint,
138 source_code: Some(source),
139 label: Some(span),
140 label_text: "error here".to_string(),
141 severity,
142 code,
143 detail,
144 where_clause,
145 schema,
146 table,
147 column,
148 datatype,
149 constraint,
150 file,
151 line,
152 routine,
153 };
154 }
155
156 Self {
157 message,
158 hint,
159 source_code: None,
160 label: None,
161 label_text: String::new(),
162 severity,
163 code,
164 detail,
165 where_clause,
166 schema,
167 table,
168 column,
169 datatype,
170 constraint,
171 file,
172 line,
173 routine,
174 }
175 }
176}
177
178pub fn format_mobc_error(error: &mobc::Error<PgError>, query: Option<&str>) -> String {
180 match error {
181 mobc::Error::Inner(PgError::Pg(error)) => format_db_error(error, query),
182 error => format!("{:?}", error),
183 }
184}
185
186pub fn format_db_error(error: &Error, query: Option<&str>) -> String {
188 match error.as_db_error() {
189 Some(db_error) => {
190 let pg_error = PgDatabaseError::from_db_error(db_error, query);
191
192 if pg_error.source_code.is_some() {
194 format!("{:?}", miette::Report::new(pg_error))
195 } else {
196 format!("{}", pg_error)
198 }
199 }
200 None => format!("{:?}", error),
201 }
202}
203
204pub fn format_error(error: &dyn std::fmt::Debug) -> String {
206 format!("{:?}", error)
207}
208
209pub fn format_miette_error(report: &miette::Report, query: Option<&str>) -> String {
211 if let Some(pg_db_error) = report.downcast_ref::<PgDatabaseError>() {
213 if pg_db_error.source_code.is_some() {
215 return format!("{:?}", report);
216 } else {
217 return format!("{}", pg_db_error);
219 }
220 }
221
222 if let Some(pg_error) = report.downcast_ref::<Error>() {
224 return format_db_error(pg_error, query);
225 }
226
227 format!("{:?}", report)
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[tokio::test]
237 async fn test_format_db_error_with_real_error() {
238 let database_url = std::env::var("DATABASE_URL")
240 .unwrap_or_else(|_| "postgresql://localhost/postgres".to_string());
241
242 let (client, connection) = tokio_postgres::connect(&database_url, tokio_postgres::NoTls)
243 .await
244 .expect("Failed to connect to database");
245
246 tokio::spawn(async move {
247 if let Err(e) = connection.await {
248 eprintln!("Connection error: {}", e);
249 }
250 });
251
252 let result = client.query("SELECT 1+;", &[]).await;
254
255 assert!(result.is_err(), "Query should have failed");
256
257 let error = result.unwrap_err();
258 let formatted = format_db_error(&error, Some("SELECT 1+;"));
259
260 assert!(formatted.contains("ERROR") || formatted.contains("syntax error"));
262 assert!(formatted.contains("Code:"));
263
264 println!("Formatted error:\n{}", formatted);
265 }
266
267 #[test]
268 fn test_pg_database_error_display() {
269 let error = PgDatabaseError {
270 message: "syntax error at end of input".to_string(),
271 hint: Some("Check your SQL syntax".to_string()),
272 source_code: None,
273 label: None,
274 label_text: String::new(),
275 severity: "ERROR".to_string(),
276 code: "42601".to_string(),
277 detail: Some("The query ended unexpectedly".to_string()),
278 where_clause: None,
279 schema: None,
280 table: Some("users".to_string()),
281 column: None,
282 datatype: None,
283 constraint: None,
284 file: Some("parser.c".to_string()),
285 line: Some(123),
286 routine: Some("parse_query".to_string()),
287 };
288
289 let output = format!("{}", error);
290 assert!(output.contains("ERROR: syntax error at end of input"));
291 assert!(output.contains("Code: 42601"));
292 assert!(output.contains("Detail: The query ended unexpectedly"));
293 assert!(output.contains("Table: users"));
294 assert!(output.contains("Hint: Check your SQL syntax"));
295 assert!(output.contains("Source: parser.c:123 in parse_query"));
296 }
297
298 #[test]
299 fn test_pg_database_error_minimal() {
300 let error = PgDatabaseError {
301 message: "column does not exist".to_string(),
302 hint: None,
303 source_code: None,
304 label: None,
305 label_text: String::new(),
306 severity: "ERROR".to_string(),
307 code: "42703".to_string(),
308 detail: None,
309 where_clause: None,
310 schema: None,
311 table: None,
312 column: None,
313 datatype: None,
314 constraint: None,
315 file: None,
316 line: None,
317 routine: None,
318 };
319
320 let output = format!("{}", error);
321 assert!(output.contains("ERROR: column does not exist"));
322 assert!(output.contains("Code: 42703"));
323 assert!(!output.contains("Detail:"));
325 assert!(!output.contains("Hint:"));
326 assert!(!output.contains("Source:"));
327 }
328
329 #[tokio::test]
330 async fn test_complete_error_output() {
331 let database_url = std::env::var("DATABASE_URL")
333 .unwrap_or_else(|_| "postgresql://localhost/postgres".to_string());
334
335 let (client, connection) = tokio_postgres::connect(&database_url, tokio_postgres::NoTls)
336 .await
337 .expect("Failed to connect to database");
338
339 tokio::spawn(async move {
340 if let Err(e) = connection.await {
341 eprintln!("Connection error: {}", e);
342 }
343 });
344
345 let result = client.query("SELECT 1+;", &[]).await;
347 if let Err(e) = result
348 && let Some(db_error) = e.as_db_error()
349 {
350 let pg_error = PgDatabaseError::from_db_error(db_error, Some("SELECT 1+;"));
351 let output = format!("{}", pg_error);
352 println!("=== Syntax Error Output ===\n{}", output);
353 assert!(output.contains("Code:"));
354 assert!(output.contains("Position:") || output.contains("Source:"));
355 }
356
357 let result = client.query("SELECT nonexistent FROM pg_class", &[]).await;
359 if let Err(e) = result
360 && let Some(db_error) = e.as_db_error()
361 {
362 let pg_error =
363 PgDatabaseError::from_db_error(db_error, Some("SELECT nonexistent FROM pg_class"));
364 let output = format!("{}", pg_error);
365 println!("=== Column Error Output ===\n{}", output);
366 assert!(output.contains("Code:"));
367 }
368 }
369
370 #[test]
371 fn test_miette_span_rendering() {
372 let query = "SELECT 1+;";
374
375 let error = PgDatabaseError {
377 message: "syntax error".to_string(),
378 hint: None,
379 source_code: Some(NamedSource::new("query", query.to_string())),
380 label: Some(miette::SourceSpan::from(3..5)),
381 label_text: "at bytes 3-5".to_string(),
382 severity: "ERROR".to_string(),
383 code: "42601".to_string(),
384 detail: None,
385 where_clause: None,
386 schema: None,
387 table: None,
388 column: None,
389 datatype: None,
390 constraint: None,
391 file: None,
392 line: None,
393 routine: None,
394 };
395
396 let report = miette::Report::new(error);
397 let formatted = format!("{:?}", report);
398 println!("Test 1 - Span at bytes 3-5:\n{}", formatted);
399
400 let error2 = PgDatabaseError {
402 message: "syntax error".to_string(),
403 hint: None,
404 source_code: Some(NamedSource::new("query", query.to_string())),
405 label: Some(miette::SourceSpan::from(9..10)),
406 label_text: "semicolon".to_string(),
407 severity: "ERROR".to_string(),
408 code: "42601".to_string(),
409 detail: None,
410 where_clause: None,
411 schema: None,
412 table: None,
413 column: None,
414 datatype: None,
415 constraint: None,
416 file: None,
417 line: None,
418 routine: None,
419 };
420
421 let report2 = miette::Report::new(error2);
422 let formatted2 = format!("{:?}", report2);
423 println!("Test 2 - Span at byte 9 (semicolon):\n{}", formatted2);
424
425 assert!(formatted.contains("SELECT"));
427 assert!(formatted2.contains("SELECT"));
428 }
429}