Skip to content

Commit e39e28c

Browse files
committed
Structured errors for PostgreSQL DB errors
Signed-off-by: itowlson <[email protected]>
1 parent caedede commit e39e28c

File tree

3 files changed

+74
-7
lines changed

3 files changed

+74
-7
lines changed

crates/factor-outbound-pg/src/client.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,48 @@ pub trait Client: Send + Sync + 'static {
9898
) -> Result<RowSet, v4::Error>;
9999
}
100100

101+
/// Extract weak-typed error data for WIT purposes
102+
fn pg_extras(dbe: &tokio_postgres::error::DbError) -> Vec<(String, String)> {
103+
let mut extras = vec![];
104+
105+
macro_rules! pg_extra {
106+
( $n:ident ) => {
107+
if let Some(value) = dbe.$n() {
108+
extras.push((stringify!($n).to_owned(), value.to_string()));
109+
}
110+
};
111+
}
112+
113+
pg_extra!(column);
114+
pg_extra!(constraint);
115+
pg_extra!(routine);
116+
pg_extra!(hint);
117+
pg_extra!(table);
118+
pg_extra!(datatype);
119+
pg_extra!(schema);
120+
pg_extra!(file);
121+
pg_extra!(line);
122+
pg_extra!(where_);
123+
124+
extras
125+
}
126+
127+
fn query_failed(e: tokio_postgres::error::Error) -> v4::Error {
128+
let flattened = format!("{e:?}");
129+
let query_error = match e.as_db_error() {
130+
None => v4::QueryError::Text(flattened),
131+
Some(dbe) => v4::QueryError::DbError(v4::DbError {
132+
as_text: flattened,
133+
severity: dbe.severity().to_owned(),
134+
code: dbe.code().code().to_owned(),
135+
message: dbe.message().to_owned(),
136+
detail: dbe.detail().map(|s| s.to_owned()),
137+
extras: pg_extras(dbe),
138+
}),
139+
};
140+
v4::Error::QueryFailed(query_error)
141+
}
142+
101143
#[async_trait]
102144
impl Client for deadpool_postgres::Object {
103145
async fn execute(
@@ -119,7 +161,7 @@ impl Client for deadpool_postgres::Object {
119161
self.as_ref()
120162
.execute(&statement, params_refs.as_slice())
121163
.await
122-
.map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))
164+
.map_err(query_failed)
123165
}
124166

125167
async fn query(
@@ -142,7 +184,7 @@ impl Client for deadpool_postgres::Object {
142184
.as_ref()
143185
.query(&statement, params_refs.as_slice())
144186
.await
145-
.map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?;
187+
.map_err(query_failed)?;
146188

147189
if results.is_empty() {
148190
return Ok(RowSet {
@@ -156,7 +198,7 @@ impl Client for deadpool_postgres::Object {
156198
.iter()
157199
.map(convert_row)
158200
.collect::<Result<Vec<_>, _>>()
159-
.map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?;
201+
.map_err(|e| v4::Error::QueryFailed(v4::QueryError::Text(format!("{e:?}"))))?;
160202

161203
Ok(RowSet { columns, rows })
162204
}

crates/world/src/conversions.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ mod rdbms_types {
376376
match error {
377377
pg4::Error::ConnectionFailed(e) => v1::postgres::PgError::ConnectionFailed(e),
378378
pg4::Error::BadParameter(e) => v1::postgres::PgError::BadParameter(e),
379-
pg4::Error::QueryFailed(e) => v1::postgres::PgError::QueryFailed(e),
379+
pg4::Error::QueryFailed(e) => v1::postgres::PgError::QueryFailed(pg_error_text(e)),
380380
pg4::Error::ValueConversionFailed(e) => {
381381
v1::postgres::PgError::ValueConversionFailed(e)
382382
}
@@ -390,7 +390,7 @@ mod rdbms_types {
390390
match error {
391391
pg4::Error::ConnectionFailed(e) => v2::rdbms_types::Error::ConnectionFailed(e),
392392
pg4::Error::BadParameter(e) => v2::rdbms_types::Error::BadParameter(e),
393-
pg4::Error::QueryFailed(e) => v2::rdbms_types::Error::QueryFailed(e),
393+
pg4::Error::QueryFailed(e) => v2::rdbms_types::Error::QueryFailed(pg_error_text(e)),
394394
pg4::Error::ValueConversionFailed(e) => {
395395
v2::rdbms_types::Error::ValueConversionFailed(e)
396396
}
@@ -404,12 +404,19 @@ mod rdbms_types {
404404
match error {
405405
pg4::Error::ConnectionFailed(e) => pg3::Error::ConnectionFailed(e),
406406
pg4::Error::BadParameter(e) => pg3::Error::BadParameter(e),
407-
pg4::Error::QueryFailed(e) => pg3::Error::QueryFailed(e),
407+
pg4::Error::QueryFailed(e) => pg3::Error::QueryFailed(pg_error_text(e)),
408408
pg4::Error::ValueConversionFailed(e) => pg3::Error::ValueConversionFailed(e),
409409
pg4::Error::Other(e) => pg3::Error::Other(e),
410410
}
411411
}
412412
}
413+
414+
pub fn pg_error_text(error: pg4::QueryError) -> String {
415+
match error {
416+
pg4::QueryError::Text(text) => text,
417+
pg4::QueryError::DbError(e) => e.as_text,
418+
}
419+
}
413420
}
414421

415422
mod postgres {

wit/deps/[email protected]/postgres.wit

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,29 @@ interface postgres {
55
variant error {
66
connection-failed(string),
77
bad-parameter(string),
8-
query-failed(string),
8+
query-failed(query-error),
99
value-conversion-failed(string),
1010
other(string)
1111
}
1212

13+
variant query-error {
14+
/// An error occurred but we do not have structured info for it
15+
text(string),
16+
/// Postgres returned a structured database error
17+
db-error(db-error),
18+
}
19+
20+
record db-error {
21+
/// Stringised version of the error. This is primarily to facilitate migration of older code.
22+
as-text: string,
23+
severity: string,
24+
code: string,
25+
message: string,
26+
detail: option<string>,
27+
/// Any error information provided by Postgres and not captured above.
28+
extras: list<tuple<string, string>>,
29+
}
30+
1331
/// Data types for a database column
1432
variant db-data-type {
1533
boolean,

0 commit comments

Comments
 (0)