From 23a1f412a1b283cf5125341e65ca2bc00f0c9245 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Fri, 19 Jul 2024 20:13:28 -0700 Subject: [PATCH] feat: introduce `DatabaseError::position()` --- sqlx-core/src/error.rs | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 042342ef..688e5445 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -193,6 +193,15 @@ pub trait DatabaseError: 'static + Send + Sync + StdError { None } + /// The position in the query where the error occurred, if applicable. + /// + /// ### Note + /// This assumes that Rust and the database server agree on the definition of "character", + /// i.e. a Unicode scalar value. + fn position(&self) -> Option { + None + } + #[doc(hidden)] fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static); @@ -320,3 +329,87 @@ macro_rules! err_protocol { $crate::error::Error::Protocol(format!($fmt, $($arg)*)) }; } + +/// The line and column (1-based) in the query where the server says an error occurred. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ErrorPosition { + /// The line number (1-based) in the query where the server says the error occurred. + pub line: usize, + /// The column (1-based) in the query where the server says the error occurred. + pub column: usize, +} + +/// The character basis for an error position. Used with [`ErrorPosition`]. +pub enum CharBasis { + /// A zero-based character index. + Zero(usize), + /// A 1-based character position. + One(usize) +} + +impl ErrorPosition { + /// Given a query string and a character position, return the line and column in the query. + /// + /// ### Note + /// This assumes that Rust and the database server agree on the definition of "character", + /// i.e. a Unicode scalar value. + pub fn from_char_pos(query: &str, char_basis: CharBasis) -> Option { + // UTF-8 encoding forces us to count characters from the beginning. + let char_idx = char_basis.to_index()?; + + let mut pos = ErrorPosition { + line: 1, + column: 1, + }; + + for (i, ch) in query.chars().enumerate() { + if i == char_idx { return Some(pos); } + + if ch == '\n' { + pos.line = pos.line.checked_add(1)?; + pos.column = 1; + } else { + pos.column = pos.column.checked_add(1)?; + } + } + + None + } +} + +impl CharBasis { + fn to_index(&self) -> Option { + match *self { + CharBasis::Zero(idx) => Some(idx), + CharBasis::One(pos) => pos.checked_sub(1), + } + } +} + +#[test] +fn test_error_position() { + macro_rules! test_error_position { + // Note: only tests one-based positions since zero-based is most of the same steps. + ($query:expr, pos: $pos:expr, line: $line:expr, col: $column:expr; $($tt:tt)*) => { + let expected = ErrorPosition { line: $line, column: $column }; + let actual = ErrorPosition::from_char_pos($query, CharBasis::One($pos)); + assert_eq!(actual, Some(expected), "for position {} in query {:?}", $pos, $query); + + test_error_position!($($tt)*); + }; + ($query:expr, pos: $pos:expr, None; $($tt:tt)*) => { + let actual = ErrorPosition::from_char_pos($query, CharBasis::One($pos)); + assert_eq!(actual, None, "for position {} in query {:?}", $pos, $query); + + test_error_position!($($tt)*); + }; + () => {} + } + + test_error_position! { + "SELECT foo", pos: 8, line: 1, col: 8; + "SELECT foo\nbar FROM baz", pos: 16, line: 2, col: 5; + "SELECT foo\r\nbar FROM baz", pos: 17, line: 2, col: 5; + "SELECT foo\r\nbar FROM baz", pos: 27, None; + } +} \ No newline at end of file