@@ -193,11 +193,22 @@ 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.
196
+ /// The line and column in the executed SQL where the error occurred,
197
+ /// if applicable and supported by the database.
197
198
///
198
199
/// ### Note
199
- /// This assumes that Rust and the database server agree on the definition of "character",
200
- /// i.e. a Unicode scalar value.
200
+ /// This may return an incorrect result if the database server disagrees with Rust
201
+ /// on the definition of a "character", i.e. a Unicode scalar value. This position should not
202
+ /// be considered authoritative.
203
+ ///
204
+ /// This also may not be returned or made readily available by every database flavor.
205
+ ///
206
+ /// For example, MySQL and MariaDB do not include the error position as a specific field
207
+ /// in the `ERR_PACKET` structure; the line number that appears in the error message is part
208
+ /// of the message string generated by the database server.
209
+ ///
210
+ /// SQLx does not attempt to parse the line number from the message string,
211
+ /// as we cannot assume that the exact message format is a stable part of the API contract.
201
212
fn position ( & self ) -> Option < ErrorPosition > {
202
213
None
203
214
}
@@ -330,40 +341,53 @@ macro_rules! err_protocol {
330
341
} ;
331
342
}
332
343
333
- /// The line and column (1-based) in the query where the server says an error occurred.
344
+ /// Details the position in an SQL string where the server says an error occurred.
334
345
#[ derive( Debug , Copy , Clone , PartialEq , Eq ) ]
335
346
pub struct ErrorPosition {
336
- /// The line number (1-based) in the query where the server says the error occurred.
347
+ /// The byte offset where the error occurred.
348
+ pub byte_offset : usize ,
349
+ /// The character (Unicode scalar value) offset where the error occurred.
350
+ pub char_offset : usize ,
351
+ /// The line number (1-based) in the string.
337
352
pub line : usize ,
338
- /// The column (1-based) in the query where the server says the error occurred .
353
+ /// The column position (1-based) in the string .
339
354
pub column : usize ,
340
355
}
341
356
342
357
/// The character basis for an error position. Used with [`ErrorPosition`].
343
- pub enum CharBasis {
358
+ #[ derive( Debug ) ]
359
+ pub enum PositionBasis {
360
+ /// A zero-based byte offset.
361
+ ByteOffset ( usize ) ,
344
362
/// A zero-based character index.
345
- Zero ( usize ) ,
363
+ CharIndex ( usize ) ,
346
364
/// A 1-based character position.
347
- One ( usize )
365
+ CharPos ( usize ) ,
348
366
}
349
367
350
368
impl ErrorPosition {
351
- /// Given a query string and a character position, return the line and column in the query.
369
+ /// Given a query string and a character basis (byte offset, 0-based index or 1-based position),
370
+ /// return the line and column.
371
+ ///
372
+ /// Returns `None` if the character basis is out-of-bounds,
373
+ /// does not lie on a character boundary (byte offsets only),
374
+ /// or overflows `usize`.
352
375
///
353
376
/// ### Note
354
377
/// This assumes that Rust and the database server agree on the definition of "character",
355
378
/// 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 ( ) ?;
379
+ pub fn find ( query : & str , basis : PositionBasis ) -> Option < ErrorPosition > {
380
+ let mut pos = ErrorPosition { byte_offset : 0 , char_offset : 0 , line : 1 , column : 1 } ;
359
381
360
- let mut pos = ErrorPosition {
361
- line : 1 ,
362
- column : 1 ,
363
- } ;
382
+ for ( char_idx, ( byte_idx, ch) ) in query. char_indices ( ) . enumerate ( ) {
383
+ pos. byte_offset = byte_idx;
384
+ pos. char_offset = char_idx;
364
385
365
- for ( i, ch) in query. chars ( ) . enumerate ( ) {
366
- if i == char_idx { return Some ( pos) ; }
386
+ // Note: since line and column are 1-based,
387
+ // we technically don't want to advance until the top of the next loop.
388
+ if pos. basis_reached ( & basis) {
389
+ return Some ( pos) ;
390
+ }
367
391
368
392
if ch == '\n' {
369
393
pos. line = pos. line . checked_add ( 1 ) ?;
@@ -373,43 +397,74 @@ impl ErrorPosition {
373
397
}
374
398
}
375
399
376
- None
400
+ // Check if the end of the string matches our basis.
401
+ pos. byte_offset = query. len ( ) ;
402
+ pos. char_offset = pos. char_offset . checked_add ( 1 ) ?;
403
+
404
+ pos. basis_reached ( & basis) . then_some ( pos)
377
405
}
378
- }
379
406
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 ) ,
407
+ fn basis_reached ( & self , basis : & PositionBasis ) -> bool {
408
+ match * basis {
409
+ PositionBasis :: ByteOffset ( offset) => {
410
+ self . byte_offset == offset
411
+ }
412
+ PositionBasis :: CharIndex ( char_idx) => {
413
+ self . char_offset == char_idx
414
+ }
415
+ PositionBasis :: CharPos ( char_pos) => {
416
+ self . char_offset . checked_add ( 1 ) == Some ( char_pos)
417
+ }
385
418
}
386
419
}
387
420
}
388
421
389
422
#[ test]
390
423
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
- }
424
+ assert_eq ! (
425
+ ErrorPosition :: find(
426
+ "SELECT foo" ,
427
+ PositionBasis :: CharPos ( 8 ) ,
428
+ ) ,
429
+ Some ( ErrorPosition {
430
+ byte_offset: 7 ,
431
+ char_offset: 7 ,
432
+ line: 1 ,
433
+ column: 8
434
+ } )
435
+ ) ;
436
+
437
+ assert_eq ! (
438
+ ErrorPosition :: find(
439
+ "SELECT foo\n bar FROM baz" ,
440
+ PositionBasis :: CharPos ( 16 ) ,
441
+ ) ,
442
+ Some ( ErrorPosition {
443
+ byte_offset: 16 ,
444
+ char_offset: 16 ,
445
+ line: 2 ,
446
+ column: 5
447
+ } )
448
+ ) ;
449
+
450
+ assert_eq ! (
451
+ ErrorPosition :: find(
452
+ "SELECT foo\r \n bar FROM baz" ,
453
+ PositionBasis :: CharPos ( 17 )
454
+ ) ,
455
+ Some ( ErrorPosition {
456
+ byte_offset: 16 ,
457
+ char_offset: 16 ,
458
+ line: 2 ,
459
+ column: 5
460
+ } )
461
+ ) ;
408
462
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
- }
463
+ assert_eq ! (
464
+ ErrorPosition :: find(
465
+ "SELECT foo\r \n bar FROM baz" ,
466
+ PositionBasis :: CharPos ( 27 )
467
+ ) ,
468
+ None
469
+ ) ;
470
+ }
0 commit comments