Skip to content

Commit 913a65f

Browse files
authored
Format position in sql (#36)
* Format position inside SQL * Add sql to display * Update error marker * Add context line before and after and use up arrow * Fix newlines * Comment code and make it more robust
1 parent c091f29 commit 913a65f

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

tokio-postgres/src/error/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub struct DbError {
8383
file: Option<Box<str>>,
8484
line: Option<u32>,
8585
routine: Option<Box<str>>,
86+
statement: Option<Box<str>>,
8687
}
8788

8889
impl DbError {
@@ -192,6 +193,7 @@ impl DbError {
192193
file,
193194
line,
194195
routine,
196+
statement: None,
195197
})
196198
}
197199

@@ -242,6 +244,43 @@ impl DbError {
242244
self.position.as_ref()
243245
}
244246

247+
/// Format the position with an arrow and at most one context line
248+
/// before and after the error.
249+
pub fn format_position(&self) -> Option<String> {
250+
let (sql, pos) = match self.position()? {
251+
ErrorPosition::Original(idx) => (self.statement.as_deref()?, *idx),
252+
ErrorPosition::Internal { position, query } => (query.as_str(), *position),
253+
};
254+
// This should not fail as long as postgres gives us a valid byte index.
255+
let (before, after) = sql.split_at_checked(pos.saturating_sub(1) as usize)?;
256+
257+
// Don't use `.lines()` because it removes the last line if it is empty.
258+
// `.split('\n')` always returns at least one item.
259+
let before: Vec<&str> = before.trim_start().split('\n').collect();
260+
let after: Vec<&str> = after.trim_end().split('\n').collect();
261+
262+
// `before.len().saturating_sub(2)..` is always in range, so unwrap would also work.
263+
let mut out = before
264+
.get(before.len().saturating_sub(2)..)
265+
.unwrap_or_default()
266+
.join("\n");
267+
268+
// `after` always has at least one item, so unwrap would also work.
269+
out.push_str(after.first().copied().unwrap_or_default());
270+
271+
// `before` always has at least one item, so unwrap would also work.
272+
// Count chars because we care about the printed width with monospace font.
273+
// This is not perfect, but good enough.
274+
let indent = before.last().copied().unwrap_or_default().chars().count();
275+
out = format!("{out}\n{:width$}^", "", width = indent);
276+
277+
if let Some(after_str) = after.get(1).copied() {
278+
out = format!("{out}\n{after_str}")
279+
}
280+
281+
Some(out)
282+
}
283+
245284
/// An indication of the context in which the error occurred.
246285
///
247286
/// Presently this includes a call stack traceback of active procedural
@@ -316,6 +355,9 @@ impl fmt::Display for DbError {
316355
if let Some(hint) = &self.hint {
317356
write!(fmt, "\nHINT: {}", hint)?;
318357
}
358+
if let Some(sql) = self.format_position() {
359+
write!(fmt, "\n{}", sql)?;
360+
}
319361
Ok(())
320362
}
321363
}
@@ -647,6 +689,13 @@ impl Error {
647689
Error::new(Kind::RowCount { expected, got })
648690
}
649691

692+
pub(crate) fn with_statement(mut self, sql: &str) -> Error {
693+
if let Kind::Db(x) = &mut self.0.kind {
694+
x.statement = Some(sql.to_owned().into_boxed_str());
695+
}
696+
self
697+
}
698+
650699
#[cfg(feature = "runtime")]
651700
pub(crate) fn connect(e: io::Error) -> Error {
652701
Error::new(Kind::Connect(e))

tokio-postgres/src/prepare.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ pub async fn prepare(
6666
let buf = encode(client, &name, query, types)?;
6767
let mut responses = client.send(RequestMessages::Single(FrontendMessage::Raw(buf)))?;
6868

69-
match responses.next().await? {
69+
match responses
70+
.next()
71+
.await
72+
.map_err(|e| e.with_statement(query))?
73+
{
7074
Message::ParseComplete => {}
7175
_ => return Err(Error::unexpected_message()),
7276
}

0 commit comments

Comments
 (0)