@@ -193,6 +193,15 @@ pub trait DatabaseError: 'static + Send + Sync + StdError {
193
193
None
194
194
}
195
195
196
+ /// The position in the query where the error occurred, if applicable.
197
+ ///
198
+ /// ### Note
199
+ /// This assumes that Rust and the database server agree on the definition of "character",
200
+ /// i.e. a Unicode scalar value.
201
+ fn position ( & self ) -> Option < ErrorPosition > {
202
+ None
203
+ }
204
+
196
205
#[ doc( hidden) ]
197
206
fn as_error ( & self ) -> & ( dyn StdError + Send + Sync + ' static ) ;
198
207
@@ -320,3 +329,87 @@ macro_rules! err_protocol {
320
329
$crate:: error:: Error :: Protocol ( format!( $fmt, $( $arg) * ) )
321
330
} ;
322
331
}
332
+
333
+ /// The line and column (1-based) in the query where the server says an error occurred.
334
+ #[ derive( Debug , Copy , Clone , PartialEq , Eq ) ]
335
+ pub struct ErrorPosition {
336
+ /// The line number (1-based) in the query where the server says the error occurred.
337
+ pub line : usize ,
338
+ /// The column (1-based) in the query where the server says the error occurred.
339
+ pub column : usize ,
340
+ }
341
+
342
+ /// The character basis for an error position. Used with [`ErrorPosition`].
343
+ pub enum CharBasis {
344
+ /// A zero-based character index.
345
+ Zero ( usize ) ,
346
+ /// A 1-based character position.
347
+ One ( usize )
348
+ }
349
+
350
+ impl ErrorPosition {
351
+ /// Given a query string and a character position, return the line and column in the query.
352
+ ///
353
+ /// ### Note
354
+ /// This assumes that Rust and the database server agree on the definition of "character",
355
+ /// i.e. a Unicode scalar value.
356
+ pub fn from_char_pos ( query : & str , char_basis : CharBasis ) -> Option < ErrorPosition > {
357
+ // UTF-8 encoding forces us to count characters from the beginning.
358
+ let char_idx = char_basis. to_index ( ) ?;
359
+
360
+ let mut pos = ErrorPosition {
361
+ line : 1 ,
362
+ column : 1 ,
363
+ } ;
364
+
365
+ for ( i, ch) in query. chars ( ) . enumerate ( ) {
366
+ if i == char_idx { return Some ( pos) ; }
367
+
368
+ if ch == '\n' {
369
+ pos. line = pos. line . checked_add ( 1 ) ?;
370
+ pos. column = 1 ;
371
+ } else {
372
+ pos. column = pos. column . checked_add ( 1 ) ?;
373
+ }
374
+ }
375
+
376
+ None
377
+ }
378
+ }
379
+
380
+ impl CharBasis {
381
+ fn to_index ( & self ) -> Option < usize > {
382
+ match * self {
383
+ CharBasis :: Zero ( idx) => Some ( idx) ,
384
+ CharBasis :: One ( pos) => pos. checked_sub ( 1 ) ,
385
+ }
386
+ }
387
+ }
388
+
389
+ #[ test]
390
+ fn test_error_position ( ) {
391
+ macro_rules! test_error_position {
392
+ // Note: only tests one-based positions since zero-based is most of the same steps.
393
+ ( $query: expr, pos: $pos: expr, line: $line: expr, col: $column: expr; $( $tt: tt) * ) => {
394
+ let expected = ErrorPosition { line: $line, column: $column } ;
395
+ let actual = ErrorPosition :: from_char_pos( $query, CharBasis :: One ( $pos) ) ;
396
+ assert_eq!( actual, Some ( expected) , "for position {} in query {:?}" , $pos, $query) ;
397
+
398
+ test_error_position!( $( $tt) * ) ;
399
+ } ;
400
+ ( $query: expr, pos: $pos: expr, None ; $( $tt: tt) * ) => {
401
+ let actual = ErrorPosition :: from_char_pos( $query, CharBasis :: One ( $pos) ) ;
402
+ assert_eq!( actual, None , "for position {} in query {:?}" , $pos, $query) ;
403
+
404
+ test_error_position!( $( $tt) * ) ;
405
+ } ;
406
+ ( ) => { }
407
+ }
408
+
409
+ test_error_position ! {
410
+ "SELECT foo" , pos: 8 , line: 1 , col: 8 ;
411
+ "SELECT foo\n bar FROM baz" , pos: 16 , line: 2 , col: 5 ;
412
+ "SELECT foo\r \n bar FROM baz" , pos: 17 , line: 2 , col: 5 ;
413
+ "SELECT foo\r \n bar FROM baz" , pos: 27 , None ;
414
+ }
415
+ }
0 commit comments