diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 042342ef9f..4ef687a510 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -193,6 +193,26 @@ pub trait DatabaseError: 'static + Send + Sync + StdError { None } + /// The line and column in the executed SQL where the error occurred, + /// if applicable and supported by the database. + /// + /// ### Note + /// This may return an incorrect result if the database server disagrees with Rust + /// on the definition of a "character", i.e. a Unicode scalar value. This position should not + /// be considered authoritative. + /// + /// This also may not be returned or made readily available by every database flavor. + /// + /// For example, MySQL and MariaDB do not include the error position as a specific field + /// in the `ERR_PACKET` structure; the line number that appears in the error message is part + /// of the message string generated by the database server. + /// + /// SQLx does not attempt to parse the line number from the message string, + /// as we cannot assume that the exact message format is a stable part of the API contract. + fn position(&self) -> Option { + None + } + #[doc(hidden)] fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static); @@ -320,3 +340,131 @@ macro_rules! err_protocol { $crate::error::Error::Protocol(format!($fmt, $($arg)*)) }; } + +/// Details the position in an SQL string where the server says an error occurred. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ErrorPosition { + /// The byte offset where the error occurred. + pub byte_offset: usize, + /// The character (Unicode scalar value) offset where the error occurred. + pub char_offset: usize, + /// The line number (1-based) in the string. + pub line: usize, + /// The column position (1-based) in the string. + pub column: usize, +} + +/// The character basis for an error position. Used with [`ErrorPosition`]. +#[derive(Debug)] +pub enum PositionBasis { + /// A zero-based byte offset. + ByteOffset(usize), + /// A zero-based character index. + CharIndex(usize), + /// A 1-based character position. + CharPos(usize), +} + +impl ErrorPosition { + /// Given a query string and a character basis (byte offset, 0-based index or 1-based position), + /// return the line and column. + /// + /// Returns `None` if the character basis is out-of-bounds, + /// does not lie on a character boundary (byte offsets only), + /// or overflows `usize`. + /// + /// ### Note + /// This assumes that Rust and the database server agree on the definition of "character", + /// i.e. a Unicode scalar value. + pub fn find(query: &str, basis: PositionBasis) -> Option { + let mut pos = ErrorPosition { byte_offset: 0, char_offset: 0, line: 1, column: 1 }; + + for (char_idx, (byte_idx, ch)) in query.char_indices().enumerate() { + pos.byte_offset = byte_idx; + pos.char_offset = char_idx; + + // Note: since line and column are 1-based, + // we technically don't want to advance until the top of the next loop. + if pos.basis_reached(&basis) { + return Some(pos); + } + + if ch == '\n' { + pos.line = pos.line.checked_add(1)?; + pos.column = 1; + } else { + pos.column = pos.column.checked_add(1)?; + } + } + + // Check if the end of the string matches our basis. + pos.byte_offset = query.len(); + pos.char_offset = pos.char_offset.checked_add(1)?; + + pos.basis_reached(&basis).then_some(pos) + } + + fn basis_reached(&self, basis: &PositionBasis) -> bool { + match *basis { + PositionBasis::ByteOffset(offset) => { + self.byte_offset == offset + } + PositionBasis::CharIndex(char_idx) => { + self.char_offset == char_idx + } + PositionBasis::CharPos(char_pos) => { + self.char_offset.checked_add(1) == Some(char_pos) + } + } + } +} + +#[test] +fn test_error_position() { + assert_eq!( + ErrorPosition::find( + "SELECT foo", + PositionBasis::CharPos(8), + ), + Some(ErrorPosition { + byte_offset: 7, + char_offset: 7, + line: 1, + column: 8 + }) + ); + + assert_eq!( + ErrorPosition::find( + "SELECT foo\nbar FROM baz", + PositionBasis::CharPos(16), + ), + Some(ErrorPosition { + byte_offset: 16, + char_offset: 16, + line: 2, + column: 5 + }) + ); + + assert_eq!( + ErrorPosition::find( + "SELECT foo\r\nbar FROM baz", + PositionBasis::CharPos(17) + ), + Some(ErrorPosition { + byte_offset: 16, + char_offset: 16, + line: 2, + column: 5 + }) + ); + + assert_eq!( + ErrorPosition::find( + "SELECT foo\r\nbar FROM baz", + PositionBasis::CharPos(27) + ), + None + ); +} diff --git a/sqlx-postgres/src/connection/executor.rs b/sqlx-postgres/src/connection/executor.rs index bb73db1e38..04102c0d33 100644 --- a/sqlx-postgres/src/connection/executor.rs +++ b/sqlx-postgres/src/connection/executor.rs @@ -1,5 +1,5 @@ use crate::describe::Describe; -use crate::error::Error; +use crate::error::{Error, PgResultExt}; use crate::executor::{Execute, Executor}; use crate::logger::QueryLogger; use crate::message::{ @@ -168,7 +168,9 @@ impl PgConnection { return Ok((*statement).clone()); } - let statement = prepare(self, sql, parameters, metadata).await?; + let statement = prepare(self, sql, parameters, metadata) + .await + .pg_find_error_pos(sql)?; if store_to_cache && self.cache_statement.is_enabled() { if let Some((id, _)) = self.cache_statement.insert(sql, statement.clone()) { @@ -267,7 +269,9 @@ impl PgConnection { Ok(try_stream! { loop { - let message = self.stream.recv().await?; + let message = self.stream.recv() + .await + .pg_find_error_pos(query)?; match message.format { MessageFormat::BindComplete diff --git a/sqlx-postgres/src/connection/stream.rs b/sqlx-postgres/src/connection/stream.rs index 0cbf405d25..786fac6e4d 100644 --- a/sqlx-postgres/src/connection/stream.rs +++ b/sqlx-postgres/src/connection/stream.rs @@ -104,7 +104,7 @@ impl PgStream { match message.format { MessageFormat::ErrorResponse => { // An error returned from the database server. - return Err(PgDatabaseError(message.decode()?).into()); + return Err(PgDatabaseError::new(message.decode()?).into()); } MessageFormat::NotificationResponse => { diff --git a/sqlx-postgres/src/error.rs b/sqlx-postgres/src/error.rs index b9df865736..b58cf32ee0 100644 --- a/sqlx-postgres/src/error.rs +++ b/sqlx-postgres/src/error.rs @@ -1,44 +1,63 @@ +use std::borrow::Cow; use std::error::Error as StdError; use std::fmt::{self, Debug, Display, Formatter}; use atoi::atoi; -use smallvec::alloc::borrow::Cow; pub(crate) use sqlx_core::error::*; use crate::message::{Notice, PgSeverity}; /// An error returned from the PostgreSQL database. -pub struct PgDatabaseError(pub(crate) Notice); +pub struct PgDatabaseError { + pub(crate) notice: Notice, + pub(crate) error_pos: Option, +} // Error message fields are documented: // https://www.postgresql.org/docs/current/protocol-error-fields.html impl PgDatabaseError { + pub(crate) fn new(notice: Notice) -> Self { + PgDatabaseError { + notice, + error_pos: None, + } + } + + pub(crate) fn find_error_pos(&mut self, query: &str) { + let error_pos = self + .pg_error_position() + .and_then(|pos_raw| pos_raw.original()) + .and_then(|pos| ErrorPosition::find(query, PositionBasis::CharPos(pos))); + + self.error_pos = error_pos; + } + #[inline] pub fn severity(&self) -> PgSeverity { - self.0.severity() + self.notice.severity() } /// The [SQLSTATE](https://www.postgresql.org/docs/current/errcodes-appendix.html) code for /// this error. #[inline] pub fn code(&self) -> &str { - self.0.code() + self.notice.code() } /// The primary human-readable error message. This should be accurate but /// terse (typically one line). #[inline] pub fn message(&self) -> &str { - self.0.message() + self.notice.message() } /// An optional secondary error message carrying more detail about the problem. /// Might run to multiple lines. #[inline] pub fn detail(&self) -> Option<&str> { - self.0.get(b'D') + self.notice.get(b'D') } /// An optional suggestion what to do about the problem. This is intended to differ from @@ -46,20 +65,20 @@ impl PgDatabaseError { /// Might run to multiple lines. #[inline] pub fn hint(&self) -> Option<&str> { - self.0.get(b'H') + self.notice.get(b'H') } - /// Indicates an error cursor position as an index into the original query string; or, + /// Indicates an error cursor position as a 1-based index into the original query string; or, /// a position into an internally generated query. #[inline] - pub fn position(&self) -> Option> { - self.0 + pub fn pg_error_position(&self) -> Option> { + self.notice .get_raw(b'P') .and_then(atoi) .map(PgErrorPosition::Original) .or_else(|| { - let position = self.0.get_raw(b'p').and_then(atoi)?; - let query = self.0.get(b'q')?; + let position = self.notice.get_raw(b'p').and_then(atoi)?; + let query = self.notice.get(b'q')?; Some(PgErrorPosition::Internal { position, query }) }) @@ -69,61 +88,61 @@ impl PgDatabaseError { /// stack traceback of active procedural language functions and internally-generated queries. /// The trace is one entry per line, most recent first. pub fn r#where(&self) -> Option<&str> { - self.0.get(b'W') + self.notice.get(b'W') } /// If this error is with a specific database object, the /// name of the schema containing that object, if any. pub fn schema(&self) -> Option<&str> { - self.0.get(b's') + self.notice.get(b's') } /// If this error is with a specific table, the name of the table. pub fn table(&self) -> Option<&str> { - self.0.get(b't') + self.notice.get(b't') } /// If the error is with a specific table column, the name of the column. pub fn column(&self) -> Option<&str> { - self.0.get(b'c') + self.notice.get(b'c') } /// If the error is with a specific data type, the name of the data type. pub fn data_type(&self) -> Option<&str> { - self.0.get(b'd') + self.notice.get(b'd') } /// If the error is with a specific constraint, the name of the constraint. /// For this purpose, indexes are constraints, even if they weren't created /// with constraint syntax. pub fn constraint(&self) -> Option<&str> { - self.0.get(b'n') + self.notice.get(b'n') } - /// The file name of the source-code location where this error was reported. + /// The file name of the server source-code location where this error was reported. pub fn file(&self) -> Option<&str> { - self.0.get(b'F') + self.notice.get(b'F') } - /// The line number of the source-code location where this error was reported. + /// The line number of the server source-code location where this error was reported. pub fn line(&self) -> Option { - self.0.get_raw(b'L').and_then(atoi) + self.notice.get_raw(b'L').and_then(atoi) } - /// The name of the source-code routine reporting this error. + /// The name of the server source-code routine reporting this error. pub fn routine(&self) -> Option<&str> { - self.0.get(b'R') + self.notice.get(b'R') } } #[derive(Debug, Eq, PartialEq)] pub enum PgErrorPosition<'a> { - /// A position (in characters) into the original query. + /// A 1-based position (in characters) into the original query. Original(usize), /// A position into the internally-generated query. Internal { - /// The position in characters. + /// The 1-based position, in characters. position: usize, /// The text of a failed internally-generated command. This could be, for example, @@ -135,12 +154,13 @@ pub enum PgErrorPosition<'a> { impl Debug for PgDatabaseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("PgDatabaseError") + .field("position", &self.error_pos) .field("severity", &self.severity()) .field("code", &self.code()) .field("message", &self.message()) .field("detail", &self.detail()) .field("hint", &self.hint()) - .field("position", &self.position()) + .field("char_position", &self.pg_error_position()) .field("where", &self.r#where()) .field("schema", &self.schema()) .field("table", &self.table()) @@ -156,7 +176,12 @@ impl Debug for PgDatabaseError { impl Display for PgDatabaseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str(self.message()) + write!(f, "(code {}", self.code())?; + if let Some(error_pos) = self.error_pos { + write!(f, ", line {}, column {}", error_pos.line, error_pos.column)?; + } + + write!(f, ") {}", self.message()) } } @@ -171,6 +196,10 @@ impl DatabaseError for PgDatabaseError { Some(Cow::Borrowed(self.code())) } + fn position(&self) -> Option { + self.error_pos + } + #[doc(hidden)] fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) { self @@ -219,6 +248,43 @@ impl DatabaseError for PgDatabaseError { } } +impl PgErrorPosition<'_> { + fn original(&self) -> Option { + match *self { + Self::Original(original) => Some(original), + _ => None, + } + } +} + +pub(crate) trait PgResultExt { + fn pg_find_error_pos(self, query: &str) -> Self; +} + +impl PgResultExt for Result { + fn pg_find_error_pos(self, query: &str) -> Self { + self.map_err(|e| { + match e { + Error::Database(e) => { + Error::Database( + // Don't panic in case this gets called in the wrong context; + // it'd be a bug, for sure, but far from a fatal one. + // The trait method has a distinct name to call this out if it happens. + e.try_downcast::().map_or_else( + |e| e, + |mut e| { + e.find_error_pos(query); + e + }, + ), + ) + } + other => other, + } + }) + } +} + /// For reference: pub(crate) mod error_codes { /// Caused when a unique or primary key is violated. diff --git a/sqlx-sqlite/src/connection/execute.rs b/sqlx-sqlite/src/connection/execute.rs index 8a76236977..346ec18d9e 100644 --- a/sqlx-sqlite/src/connection/execute.rs +++ b/sqlx-sqlite/src/connection/execute.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use crate::connection::{ConnectionHandle, ConnectionState}; use crate::error::Error; use crate::logger::QueryLogger; @@ -20,14 +21,14 @@ pub struct ExecuteIter<'a> { pub(crate) fn iter<'a>( conn: &'a mut ConnectionState, - query: &'a str, + query: &'a Arc, args: Option>, persistent: bool, ) -> Result, Error> { // fetch the cached statement or allocate a new one - let statement = conn.statements.get(query, persistent)?; + let statement = conn.statements.get(query.clone(), persistent)?; - let logger = QueryLogger::new(query, conn.log_settings.clone()); + let logger = QueryLogger::new(&query, conn.log_settings.clone()); Ok(ExecuteIter { handle: &mut conn.handle, diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index 621eed490d..f9209e8d29 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -6,7 +6,7 @@ use std::os::raw::{c_int, c_void}; use std::panic::catch_unwind; use std::ptr; use std::ptr::NonNull; - +use std::sync::Arc; use futures_core::future::BoxFuture; use futures_intrusive::sync::MutexGuard; use futures_util::future; @@ -395,12 +395,12 @@ impl Statements { } } - fn get(&mut self, query: &str, persistent: bool) -> Result<&mut VirtualStatement, Error> { + fn get(&mut self, query: Arc, persistent: bool) -> Result<&mut VirtualStatement, Error> { if !persistent || !self.cached.is_enabled() { return Ok(self.temp.insert(VirtualStatement::new(query, false)?)); } - let exists = self.cached.contains_key(query); + let exists = self.cached.contains_key(&query); if !exists { let statement = VirtualStatement::new(query, true)?; diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index 18e34aae86..1afa3dbfc7 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -40,15 +40,15 @@ pub(crate) struct WorkerSharedState { enum Command { Prepare { - query: Box, + query: Arc, tx: oneshot::Sender, Error>>, }, Describe { - query: Box, + query: Arc, tx: oneshot::Sender, Error>>, }, Execute { - query: Box, + query: Arc, arguments: Option>, persistent: bool, tx: flume::Sender, Error>>, @@ -119,7 +119,7 @@ impl ConnectionWorker { let _guard = span.enter(); match cmd { Command::Prepare { query, tx } => { - tx.send(prepare(&mut conn, &query).map(|prepared| { + tx.send(prepare(&mut conn, query).map(|prepared| { update_cached_statements_size( &conn, &shared.cached_statements_size, @@ -394,7 +394,7 @@ impl ConnectionWorker { } } -fn prepare(conn: &mut ConnectionState, query: &str) -> Result, Error> { +fn prepare(conn: &mut ConnectionState, query: Arc) -> Result, Error> { // prepare statement object (or checkout from cache) let statement = conn.statements.get(query, true)?; diff --git a/sqlx-sqlite/src/error.rs b/sqlx-sqlite/src/error.rs index c00374fe60..1d0262b84e 100644 --- a/sqlx-sqlite/src/error.rs +++ b/sqlx-sqlite/src/error.rs @@ -5,9 +5,9 @@ use std::os::raw::c_int; use std::{borrow::Cow, str::from_utf8_unchecked}; use libsqlite3_sys::{ - sqlite3, sqlite3_errmsg, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK, - SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, - SQLITE_CONSTRAINT_UNIQUE, + sqlite3, sqlite3_errmsg, sqlite3_error_offset, sqlite3_extended_errcode, + SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, + SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_UNIQUE, }; pub(crate) use sqlx_core::error::*; @@ -19,6 +19,8 @@ pub(crate) use sqlx_core::error::*; pub struct SqliteError { code: c_int, message: String, + offset: Option, + error_pos: Option, } impl SqliteError { @@ -34,9 +36,26 @@ impl SqliteError { from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()) }; + // returns `-1` if not applicable + let offset = unsafe { sqlite3_error_offset(handle) }.try_into().ok(); + Self { code, message: message.to_owned(), + offset, + error_pos, + } + } + + pub(crate) fn add_offset(&mut self, offset: usize) { + if let Some(prev_offset) = self.offset { + self.offset = prev_offset.checked_add(offset); + } + } + + pub(crate) fn find_error_pos(&mut self, query: &str) { + if let Some(offset) = self.offset { + self.error_pos = ErrorPosition::find(query, PositionBasis::ByteOffset(offset)); } } @@ -72,6 +91,10 @@ impl DatabaseError for SqliteError { Some(format!("{}", self.code).into()) } + fn position(&self) -> Option { + self.error_pos + } + #[doc(hidden)] fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) { self diff --git a/sqlx-sqlite/src/statement/virtual.rs b/sqlx-sqlite/src/statement/virtual.rs index e37215bc9e..b552609533 100644 --- a/sqlx-sqlite/src/statement/virtual.rs +++ b/sqlx-sqlite/src/statement/virtual.rs @@ -31,8 +31,11 @@ pub struct VirtualStatement { /// there are no more statements to execute and `reset()` must be called index: Option, - /// tail of the most recently prepared SQL statement within this container - tail: Bytes, + /// The full input SQL. + sql: Arc, + + /// The byte offset of the next statement to prepare in `sql`. + tail_offset: usize, /// underlying sqlite handles for each inner statement /// a SQL query string in SQLite is broken up into N statements @@ -44,6 +47,9 @@ pub struct VirtualStatement { // each set of column names pub(crate) column_names: SmallVec<[Arc>; 1]>, + + /// Offsets into `sql` for each statement. + pub(crate) sql_offsets: SmallVec<[usize; 1]>, } pub struct PreparedStatement<'a> { @@ -53,9 +59,7 @@ pub struct PreparedStatement<'a> { } impl VirtualStatement { - pub(crate) fn new(mut query: &str, persistent: bool) -> Result { - query = query.trim(); - + pub(crate) fn new(query: Arc, persistent: bool) -> Result { if query.len() > i32::max_value() as usize { return Err(err_protocol!( "query string must be smaller than {} bytes", @@ -65,11 +69,13 @@ impl VirtualStatement { Ok(Self { persistent, - tail: Bytes::from(String::from(query)), + sql: query, + tail_offset: 0, handles: SmallVec::with_capacity(1), index: None, columns: SmallVec::with_capacity(1), column_names: SmallVec::with_capacity(1), + sql_offsets: SmallVec::with_capacity(1), }) } @@ -84,11 +90,33 @@ impl VirtualStatement { .or(Some(0)); while self.handles.len() <= self.index.unwrap_or(0) { - if self.tail.is_empty() { + let sql_offset = self.tail_offset; + + let query = self.sql.get(sql_offset..).unwrap_or(""); + + if query.is_empty() { return Ok(None); } - if let Some(statement) = prepare(conn.as_ptr(), &mut self.tail, self.persistent)? { + let (consumed, maybe_statement) = try_prepare( + conn.as_ptr(), + query, + self.persistent, + ).map_err(|mut e| { + // `sqlite3_offset()` returns the offset into the passed string, + // but we want the offset into the original SQL string. + e.add_offset(sql_offset); + e.find_error_pos(&self.sql); + e + })?; + + self.tail_offset = self.tail_offset + .checked_add(consumed) + // Highly unlikely, but since we're dealing with `unsafe` here + // it's best not to fool around. + .ok_or_else(|| Error::Protocol(format!("overflow adding {n:?} bytes to tail_offset {tail_offset:?}")))?; + + if let Some(statement) = maybe_statement { let num = statement.column_count(); let mut columns = Vec::with_capacity(num); @@ -112,6 +140,7 @@ impl VirtualStatement { self.handles.push(statement); self.columns.push(Arc::new(columns)); self.column_names.push(Arc::new(column_names)); + self.sql_offsets.push(sql_offset); } } @@ -140,11 +169,13 @@ impl VirtualStatement { } } -fn prepare( +/// Attempt to prepare one statement, returning the number of bytes consumed from `sql`, +/// and the statement handle if successful. +fn try_prepare( conn: *mut sqlite3, - query: &mut Bytes, + query: &str, persistent: bool, -) -> Result, Error> { +) -> Result<(usize, Option), SqliteError> { let mut flags = 0; // For some reason, when building with the `sqlcipher` feature enabled @@ -158,40 +189,37 @@ fn prepare( flags |= SQLITE_PREPARE_PERSISTENT as u32; } - while !query.is_empty() { - let mut statement_handle: *mut sqlite3_stmt = null_mut(); - let mut tail: *const c_char = null(); - - let query_ptr = query.as_ptr() as *const c_char; - let query_len = query.len() as i32; - - // - let status = unsafe { - sqlite3_prepare_v3( - conn, - query_ptr, - query_len, - flags, - &mut statement_handle, - &mut tail, - ) - }; - - if status != SQLITE_OK { - return Err(SqliteError::new(conn).into()); - } - - // tail should point to the first byte past the end of the first SQL - // statement in zSql. these routines only compile the first statement, - // so tail is left pointing to what remains un-compiled. + let mut statement_handle: *mut sqlite3_stmt = null_mut(); + let mut tail_ptr: *const c_char = null(); + + let query_ptr = query.as_ptr() as *const c_char; + let query_len = query.len() as i32; + + // + let status = unsafe { + sqlite3_prepare_v3( + conn, + query_ptr, + query_len, + flags, + &mut statement_handle, + &mut tail_ptr, + ) + }; + + if status != SQLITE_OK { + // Note: `offset` and `error_pos` will be updated in `VirtualStatement::prepare_next()`. + return Err(SqliteError::new(conn)); + } - let n = (tail as usize) - (query_ptr as usize); - query.advance(n); + // tail should point to the first byte past the end of the first SQL + // statement in zSql. these routines only compile the first statement, + // so tail is left pointing to what remains un-compiled. - if let Some(handle) = NonNull::new(statement_handle) { - return Ok(Some(StatementHandle::new(handle))); - } - } + let consumed = (tail_ptr as usize) - (query_ptr as usize); - Ok(None) + Ok(( + consumed, + NonNull::new(statement_handle).map(StatementHandle::new), + )) } diff --git a/tests/postgres/error.rs b/tests/postgres/error.rs index d6f78140da..830ee721a0 100644 --- a/tests/postgres/error.rs +++ b/tests/postgres/error.rs @@ -1,4 +1,5 @@ use sqlx::{error::ErrorKind, postgres::Postgres, Connection}; +use sqlx_core::executor::Executor; use sqlx_test::new; #[sqlx_macros::test] @@ -74,3 +75,70 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + + +#[sqlx::test] +async fn test_error_includes_position() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let err: sqlx::Error = conn + .prepare("SELECT bar.foo as foo\nFORM bar") + .await + .unwrap_err(); + + let sqlx::Error::Database(dbe) = err else { + panic!("unexpected error kind {err:?}") + }; + + let pos = dbe.position().unwrap(); + + assert_eq!(pos.line, 2); + assert_eq!(pos.column, 1); + assert!( + dbe.to_string().contains("line 2, column 1"), + "{:?}", + dbe.to_string() + ); + + let err: sqlx::Error = sqlx::query("SELECT bar.foo as foo\r\nFORM bar") + .execute(&mut conn) + .await + .unwrap_err(); + + let sqlx::Error::Database(dbe) = err else { + panic!("unexpected error kind {err:?}") + }; + + let pos = dbe.position().unwrap(); + + assert_eq!(pos.line, 2); + assert_eq!(pos.column, 1); + assert!( + dbe.to_string().contains("line 2, column 1"), + "{:?}", + dbe.to_string() + ); + + let err: sqlx::Error = sqlx::query( + "SELECT foo\r\nFROM bar\r\nINNER JOIN baz USING (foo)\r\nWHERE foo=1 ADN baz.foo = 2", + ) + .execute(&mut conn) + .await + .unwrap_err(); + + let sqlx::Error::Database(dbe) = err else { + panic!("unexpected error kind {err:?}") + }; + + let pos = dbe.position().unwrap(); + + assert_eq!(pos.line, 4); + assert_eq!(pos.column, 13); + assert!( + dbe.to_string().contains("line 4, column 13"), + "{:?}", + dbe.to_string() + ); + + Ok(()) +} \ No newline at end of file diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 7edb5a7a8c..77e495c4c5 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -116,7 +116,7 @@ async fn it_can_inspect_errors() -> anyhow::Result<()> { assert_eq!(err.severity(), PgSeverity::Error); assert_eq!(err.message(), "column \"f\" does not exist"); assert_eq!(err.code(), "42703"); - assert_eq!(err.position(), Some(PgErrorPosition::Original(8))); + assert_eq!(err.pg_error_position(), Some(PgErrorPosition::Original(8))); assert_eq!(err.routine(), Some("errorMissingColumn")); assert_eq!(err.constraint(), None); @@ -151,7 +151,7 @@ async fn it_can_inspect_constraint_errors() -> anyhow::Result<()> { "new row for relation \"products\" violates check constraint \"products_price_check\"" ); assert_eq!(err.code(), "23514"); - assert_eq!(err.position(), None); + assert_eq!(err.pg_error_position(), None); assert_eq!(err.routine(), Some("ExecConstraints")); assert_eq!(err.constraint(), Some("products_price_check")); diff --git a/tests/sqlite/error.rs b/tests/sqlite/error.rs index 1f6b797e69..4d93e633f1 100644 --- a/tests/sqlite/error.rs +++ b/tests/sqlite/error.rs @@ -1,4 +1,5 @@ use sqlx::{error::ErrorKind, sqlite::Sqlite, Connection, Executor}; +use sqlx_sqlite::{SqliteConnection, SqliteError}; use sqlx_test::new; #[sqlx_macros::test] @@ -70,3 +71,24 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_useful_information() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect(":memory:").await?; + + let err: sqlx::Error = sqlx::query("SELECT foo FORM bar") + .execute(&mut conn) + .await + .unwrap_err(); + + let sqlx::Error::Database(dbe) = err else { + panic!("unexpected error kind: {err:?}") + }; + + let dbe= dbe.downcast::(); + + eprintln!("{dbe}"); + eprintln!("{dbe:?}"); + + Ok(()) +} \ No newline at end of file