@@ -83,6 +83,7 @@ pub struct DbError {
83
83
file : Option < Box < str > > ,
84
84
line : Option < u32 > ,
85
85
routine : Option < Box < str > > ,
86
+ statement : Option < Box < str > > ,
86
87
}
87
88
88
89
impl DbError {
@@ -192,6 +193,7 @@ impl DbError {
192
193
file,
193
194
line,
194
195
routine,
196
+ statement : None ,
195
197
} )
196
198
}
197
199
@@ -242,6 +244,43 @@ impl DbError {
242
244
self . position . as_ref ( )
243
245
}
244
246
247
+ /// Format the position with an arrow and at most one context line
248
+ /// before and after the error.
249
+ pub fn format_position ( & self ) -> Option < String > {
250
+ let ( sql, pos) = match self . position ( ) ? {
251
+ ErrorPosition :: Original ( idx) => ( self . statement . as_deref ( ) ?, * idx) ,
252
+ ErrorPosition :: Internal { position, query } => ( query. as_str ( ) , * position) ,
253
+ } ;
254
+ // This should not fail as long as postgres gives us a valid byte index.
255
+ let ( before, after) = sql. split_at_checked ( pos. saturating_sub ( 1 ) as usize ) ?;
256
+
257
+ // Don't use `.lines()` because it removes the last line if it is empty.
258
+ // `.split('\n')` always returns at least one item.
259
+ let before: Vec < & str > = before. trim_start ( ) . split ( '\n' ) . collect ( ) ;
260
+ let after: Vec < & str > = after. trim_end ( ) . split ( '\n' ) . collect ( ) ;
261
+
262
+ // `before.len().saturating_sub(2)..` is always in range, so unwrap would also work.
263
+ let mut out = before
264
+ . get ( before. len ( ) . saturating_sub ( 2 ) ..)
265
+ . unwrap_or_default ( )
266
+ . join ( "\n " ) ;
267
+
268
+ // `after` always has at least one item, so unwrap would also work.
269
+ out. push_str ( after. first ( ) . copied ( ) . unwrap_or_default ( ) ) ;
270
+
271
+ // `before` always has at least one item, so unwrap would also work.
272
+ // Count chars because we care about the printed width with monospace font.
273
+ // This is not perfect, but good enough.
274
+ let indent = before. last ( ) . copied ( ) . unwrap_or_default ( ) . chars ( ) . count ( ) ;
275
+ out = format ! ( "{out}\n {:width$}^" , "" , width = indent) ;
276
+
277
+ if let Some ( after_str) = after. get ( 1 ) . copied ( ) {
278
+ out = format ! ( "{out}\n {after_str}" )
279
+ }
280
+
281
+ Some ( out)
282
+ }
283
+
245
284
/// An indication of the context in which the error occurred.
246
285
///
247
286
/// Presently this includes a call stack traceback of active procedural
@@ -316,6 +355,9 @@ impl fmt::Display for DbError {
316
355
if let Some ( hint) = & self . hint {
317
356
write ! ( fmt, "\n HINT: {}" , hint) ?;
318
357
}
358
+ if let Some ( sql) = self . format_position ( ) {
359
+ write ! ( fmt, "\n {}" , sql) ?;
360
+ }
319
361
Ok ( ( ) )
320
362
}
321
363
}
@@ -647,6 +689,13 @@ impl Error {
647
689
Error :: new ( Kind :: RowCount { expected, got } )
648
690
}
649
691
692
+ pub ( crate ) fn with_statement ( mut self , sql : & str ) -> Error {
693
+ if let Kind :: Db ( x) = & mut self . 0 . kind {
694
+ x. statement = Some ( sql. to_owned ( ) . into_boxed_str ( ) ) ;
695
+ }
696
+ self
697
+ }
698
+
650
699
#[ cfg( feature = "runtime" ) ]
651
700
pub ( crate ) fn connect ( e : io:: Error ) -> Error {
652
701
Error :: new ( Kind :: Connect ( e) )
0 commit comments