Skip to content

Commit c2c9d9f

Browse files
committed
feat: introduce DatabaseError::position()
1 parent b37b34b commit c2c9d9f

File tree

1 file changed

+93
-0
lines changed

1 file changed

+93
-0
lines changed

sqlx-core/src/error.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ pub trait DatabaseError: 'static + Send + Sync + StdError {
193193
None
194194
}
195195

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+
196205
#[doc(hidden)]
197206
fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static);
198207

@@ -320,3 +329,87 @@ macro_rules! err_protocol {
320329
$crate::error::Error::Protocol(format!($fmt, $($arg)*))
321330
};
322331
}
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\nbar FROM baz", pos: 16, line: 2, col: 5;
412+
"SELECT foo\r\nbar FROM baz", pos: 17, line: 2, col: 5;
413+
"SELECT foo\r\nbar FROM baz", pos: 27, None;
414+
}
415+
}

0 commit comments

Comments
 (0)