Skip to content

Commit 08e620b

Browse files
committed
breaking(postgres): add position information to errors
Breaking change: renamed `PgDatabaseError::position()` to `::position_raw()`.
1 parent c2c9d9f commit 08e620b

File tree

4 files changed

+157
-28
lines changed

4 files changed

+157
-28
lines changed

sqlx-postgres/src/connection/executor.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::describe::Describe;
2-
use crate::error::Error;
2+
use crate::error::{Error, PgResultExt};
33
use crate::executor::{Execute, Executor};
44
use crate::logger::QueryLogger;
55
use crate::message::{
@@ -168,7 +168,9 @@ impl PgConnection {
168168
return Ok((*statement).clone());
169169
}
170170

171-
let statement = prepare(self, sql, parameters, metadata).await?;
171+
let statement = prepare(self, sql, parameters, metadata)
172+
.await
173+
.pg_apply_error_pos(sql)?;
172174

173175
if store_to_cache && self.cache_statement.is_enabled() {
174176
if let Some((id, _)) = self.cache_statement.insert(sql, statement.clone()) {
@@ -267,7 +269,9 @@ impl PgConnection {
267269

268270
Ok(try_stream! {
269271
loop {
270-
let message = self.stream.recv().await?;
272+
let message = self.stream.recv()
273+
.await
274+
.pg_apply_error_pos(query)?;
271275

272276
match message.format {
273277
MessageFormat::BindComplete

sqlx-postgres/src/connection/stream.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl PgStream {
104104
match message.format {
105105
MessageFormat::ErrorResponse => {
106106
// An error returned from the database server.
107-
return Err(PgDatabaseError(message.decode()?).into());
107+
return Err(PgDatabaseError::new(message.decode()?).into());
108108
}
109109

110110
MessageFormat::NotificationResponse => {

sqlx-postgres/src/error.rs

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,84 @@
1+
use std::borrow::Cow;
12
use std::error::Error as StdError;
23
use std::fmt::{self, Debug, Display, Formatter};
34

45
use atoi::atoi;
5-
use smallvec::alloc::borrow::Cow;
66

77
pub(crate) use sqlx_core::error::*;
88

99
use crate::message::{Notice, PgSeverity};
1010

1111
/// An error returned from the PostgreSQL database.
12-
pub struct PgDatabaseError(pub(crate) Notice);
12+
pub struct PgDatabaseError {
13+
pub(crate) notice: Notice,
14+
pub(crate) error_pos: Option<ErrorPosition>,
15+
}
1316

1417
// Error message fields are documented:
1518
// https://www.postgresql.org/docs/current/protocol-error-fields.html
1619

1720
impl PgDatabaseError {
21+
pub(crate) fn new(notice: Notice) -> Self {
22+
PgDatabaseError {
23+
notice,
24+
error_pos: None,
25+
}
26+
}
27+
28+
pub(crate) fn apply_error_pos(&mut self, query: &str) {
29+
let error_pos = self
30+
.position_raw()
31+
.and_then(|pos_raw| pos_raw.original())
32+
.and_then(|pos| ErrorPosition::from_char_pos(query, CharBasis::One(pos)));
33+
34+
self.error_pos = error_pos;
35+
}
36+
1837
#[inline]
1938
pub fn severity(&self) -> PgSeverity {
20-
self.0.severity()
39+
self.notice.severity()
2140
}
2241

2342
/// The [SQLSTATE](https://www.postgresql.org/docs/current/errcodes-appendix.html) code for
2443
/// this error.
2544
#[inline]
2645
pub fn code(&self) -> &str {
27-
self.0.code()
46+
self.notice.code()
2847
}
2948

3049
/// The primary human-readable error message. This should be accurate but
3150
/// terse (typically one line).
3251
#[inline]
3352
pub fn message(&self) -> &str {
34-
self.0.message()
53+
self.notice.message()
3554
}
3655

3756
/// An optional secondary error message carrying more detail about the problem.
3857
/// Might run to multiple lines.
3958
#[inline]
4059
pub fn detail(&self) -> Option<&str> {
41-
self.0.get(b'D')
60+
self.notice.get(b'D')
4261
}
4362

4463
/// An optional suggestion what to do about the problem. This is intended to differ from
4564
/// `detail` in that it offers advice (potentially inappropriate) rather than hard facts.
4665
/// Might run to multiple lines.
4766
#[inline]
4867
pub fn hint(&self) -> Option<&str> {
49-
self.0.get(b'H')
68+
self.notice.get(b'H')
5069
}
5170

5271
/// Indicates an error cursor position as an index into the original query string; or,
5372
/// a position into an internally generated query.
5473
#[inline]
55-
pub fn position(&self) -> Option<PgErrorPosition<'_>> {
56-
self.0
74+
pub fn position_raw(&self) -> Option<PgErrorPosition<'_>> {
75+
self.notice
5776
.get_raw(b'P')
5877
.and_then(atoi)
5978
.map(PgErrorPosition::Original)
6079
.or_else(|| {
61-
let position = self.0.get_raw(b'p').and_then(atoi)?;
62-
let query = self.0.get(b'q')?;
80+
let position = self.notice.get_raw(b'p').and_then(atoi)?;
81+
let query = self.notice.get(b'q')?;
6382

6483
Some(PgErrorPosition::Internal { position, query })
6584
})
@@ -69,50 +88,50 @@ impl PgDatabaseError {
6988
/// stack traceback of active procedural language functions and internally-generated queries.
7089
/// The trace is one entry per line, most recent first.
7190
pub fn r#where(&self) -> Option<&str> {
72-
self.0.get(b'W')
91+
self.notice.get(b'W')
7392
}
7493

7594
/// If this error is with a specific database object, the
7695
/// name of the schema containing that object, if any.
7796
pub fn schema(&self) -> Option<&str> {
78-
self.0.get(b's')
97+
self.notice.get(b's')
7998
}
8099

81100
/// If this error is with a specific table, the name of the table.
82101
pub fn table(&self) -> Option<&str> {
83-
self.0.get(b't')
102+
self.notice.get(b't')
84103
}
85104

86105
/// If the error is with a specific table column, the name of the column.
87106
pub fn column(&self) -> Option<&str> {
88-
self.0.get(b'c')
107+
self.notice.get(b'c')
89108
}
90109

91110
/// If the error is with a specific data type, the name of the data type.
92111
pub fn data_type(&self) -> Option<&str> {
93-
self.0.get(b'd')
112+
self.notice.get(b'd')
94113
}
95114

96115
/// If the error is with a specific constraint, the name of the constraint.
97116
/// For this purpose, indexes are constraints, even if they weren't created
98117
/// with constraint syntax.
99118
pub fn constraint(&self) -> Option<&str> {
100-
self.0.get(b'n')
119+
self.notice.get(b'n')
101120
}
102121

103122
/// The file name of the source-code location where this error was reported.
104123
pub fn file(&self) -> Option<&str> {
105-
self.0.get(b'F')
124+
self.notice.get(b'F')
106125
}
107126

108127
/// The line number of the source-code location where this error was reported.
109128
pub fn line(&self) -> Option<usize> {
110-
self.0.get_raw(b'L').and_then(atoi)
129+
self.notice.get_raw(b'L').and_then(atoi)
111130
}
112131

113132
/// The name of the source-code routine reporting this error.
114133
pub fn routine(&self) -> Option<&str> {
115-
self.0.get(b'R')
134+
self.notice.get(b'R')
116135
}
117136
}
118137

@@ -135,12 +154,13 @@ pub enum PgErrorPosition<'a> {
135154
impl Debug for PgDatabaseError {
136155
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
137156
f.debug_struct("PgDatabaseError")
157+
.field("position", &self.error_pos)
138158
.field("severity", &self.severity())
139159
.field("code", &self.code())
140160
.field("message", &self.message())
141161
.field("detail", &self.detail())
142162
.field("hint", &self.hint())
143-
.field("position", &self.position())
163+
.field("position_raw", &self.position())
144164
.field("where", &self.r#where())
145165
.field("schema", &self.schema())
146166
.field("table", &self.table())
@@ -156,7 +176,12 @@ impl Debug for PgDatabaseError {
156176

157177
impl Display for PgDatabaseError {
158178
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
159-
f.write_str(self.message())
179+
write!(f, "(code {}", self.code())?;
180+
if let Some(error_pos) = self.error_pos {
181+
write!(f, ", line {}, column {}", error_pos.line, error_pos.column)?;
182+
}
183+
184+
write!(f, ") {}", self.message())
160185
}
161186
}
162187

@@ -171,6 +196,10 @@ impl DatabaseError for PgDatabaseError {
171196
Some(Cow::Borrowed(self.code()))
172197
}
173198

199+
fn position(&self) -> Option<ErrorPosition> {
200+
self.error_pos
201+
}
202+
174203
#[doc(hidden)]
175204
fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) {
176205
self
@@ -219,6 +248,43 @@ impl DatabaseError for PgDatabaseError {
219248
}
220249
}
221250

251+
impl PgErrorPosition<'_> {
252+
fn original(&self) -> Option<usize> {
253+
match *self {
254+
Self::Original(original) => Some(original),
255+
_ => None,
256+
}
257+
}
258+
}
259+
260+
pub(crate) trait PgResultExt {
261+
fn pg_apply_error_pos(self, query: &str) -> Self;
262+
}
263+
264+
impl<T> PgResultExt for Result<T, Error> {
265+
fn pg_apply_error_pos(self, query: &str) -> Self {
266+
self.map_err(|e| {
267+
match e {
268+
Error::Database(e) => {
269+
Error::Database(
270+
// Don't panic in case this gets called in the wrong context;
271+
// it'd be a bug, for sure, but far from a fatal one.
272+
// I gave the method a distinct name to call this out if it happens.
273+
e.try_downcast::<PgDatabaseError>().map_or_else(
274+
|e| e,
275+
|mut e| {
276+
e.apply_error_pos(query);
277+
e
278+
},
279+
),
280+
)
281+
}
282+
other => other,
283+
}
284+
})
285+
}
286+
}
287+
222288
/// For reference: <https://www.postgresql.org/docs/current/errcodes-appendix.html>
223289
pub(crate) mod error_codes {
224290
/// Caused when a unique or primary key is violated.

tests/postgres/postgres.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async fn it_can_inspect_errors() -> anyhow::Result<()> {
116116
assert_eq!(err.severity(), PgSeverity::Error);
117117
assert_eq!(err.message(), "column \"f\" does not exist");
118118
assert_eq!(err.code(), "42703");
119-
assert_eq!(err.position(), Some(PgErrorPosition::Original(8)));
119+
assert_eq!(err.position_raw(), Some(PgErrorPosition::Original(8)));
120120
assert_eq!(err.routine(), Some("errorMissingColumn"));
121121
assert_eq!(err.constraint(), None);
122122

@@ -151,7 +151,7 @@ async fn it_can_inspect_constraint_errors() -> anyhow::Result<()> {
151151
"new row for relation \"products\" violates check constraint \"products_price_check\""
152152
);
153153
assert_eq!(err.code(), "23514");
154-
assert_eq!(err.position(), None);
154+
assert_eq!(err.position_raw(), None);
155155
assert_eq!(err.routine(), Some("ExecConstraints"));
156156
assert_eq!(err.constraint(), Some("products_price_check"));
157157

@@ -1934,3 +1934,62 @@ async fn test_issue_3052() {
19341934
"expected encode error, got {too_large_error:?}",
19351935
);
19361936
}
1937+
1938+
#[sqlx::test]
1939+
async fn test_error_includes_position() -> anyhow::Result<()> {
1940+
let mut conn = new::<Postgres>().await?;
1941+
1942+
let err: sqlx::Error = conn.prepare(
1943+
"SELECT bar.foo as foo\nFORM bar"
1944+
)
1945+
.await
1946+
.unwrap_err();
1947+
1948+
let sqlx::Error::Database(dbe) = err else {
1949+
panic!("unexpected error kind {err:?}")
1950+
};
1951+
1952+
let pos = dbe.position().unwrap();
1953+
1954+
assert_eq!(pos.line, 2);
1955+
assert_eq!(pos.column, 1);
1956+
assert!(dbe.to_string().contains("line 2, column 1"), "{:?}", dbe.to_string());
1957+
1958+
let err: sqlx::Error = sqlx::query(
1959+
"SELECT bar.foo as foo\r\nFORM bar"
1960+
)
1961+
.execute(&mut conn)
1962+
.await
1963+
.unwrap_err();
1964+
1965+
let sqlx::Error::Database(dbe) = err else {
1966+
panic!("unexpected error kind {err:?}")
1967+
};
1968+
1969+
let pos = dbe.position().unwrap();
1970+
1971+
assert_eq!(pos.line, 2);
1972+
assert_eq!(pos.column, 1);
1973+
assert!(dbe.to_string().contains("line 2, column 1"), "{:?}", dbe.to_string());
1974+
1975+
1976+
let err: sqlx::Error = sqlx::query(
1977+
"SELECT foo\r\nFROM bar\r\nINNER JOIN baz USING (foo)\r\nWHERE foo=1 ADN baz.foo = 2"
1978+
)
1979+
.execute(&mut conn)
1980+
.await
1981+
.unwrap_err();
1982+
1983+
let sqlx::Error::Database(dbe) = err else {
1984+
panic!("unexpected error kind {err:?}")
1985+
};
1986+
1987+
let pos = dbe.position().unwrap();
1988+
1989+
assert_eq!(pos.line, 4);
1990+
assert_eq!(pos.column, 13);
1991+
assert!(dbe.to_string().contains("line 4, column 13"), "{:?}", dbe.to_string());
1992+
1993+
1994+
Ok(())
1995+
}

0 commit comments

Comments
 (0)