Skip to content

Commit 69fb432

Browse files
committed
Structured errors for PostgreSQL DB errors
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
1 parent 86ea8df commit 69fb432

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
@@ -29,6 +29,48 @@ pub trait Client {
2929
) -> Result<RowSet, v4::Error>;
3030
}
3131

32+
/// Extract weak-typed error data for WIT purposes
33+
fn pg_extras(dbe: &tokio_postgres::error::DbError) -> Vec<(String, String)> {
34+
let mut extras = vec![];
35+
36+
macro_rules! pg_extra {
37+
( $n:ident ) => {
38+
if let Some(value) = dbe.$n() {
39+
extras.push((stringify!($n).to_owned(), value.to_string()));
40+
}
41+
};
42+
}
43+
44+
pg_extra!(column);
45+
pg_extra!(constraint);
46+
pg_extra!(routine);
47+
pg_extra!(hint);
48+
pg_extra!(table);
49+
pg_extra!(datatype);
50+
pg_extra!(schema);
51+
pg_extra!(file);
52+
pg_extra!(line);
53+
pg_extra!(where_);
54+
55+
extras
56+
}
57+
58+
fn query_failed(e: tokio_postgres::error::Error) -> v4::Error {
59+
let flattened = format!("{e:?}");
60+
let query_error = match e.as_db_error() {
61+
None => v4::QueryError::Text(flattened),
62+
Some(dbe) => v4::QueryError::DbError(v4::DbError {
63+
as_text: flattened,
64+
severity: dbe.severity().to_owned(),
65+
code: dbe.code().code().to_owned(),
66+
message: dbe.message().to_owned(),
67+
detail: dbe.detail().map(|s| s.to_owned()),
68+
extras: pg_extras(dbe),
69+
}),
70+
};
71+
v4::Error::QueryFailed(query_error)
72+
}
73+
3274
#[async_trait]
3375
impl Client for TokioClient {
3476
async fn build_client(address: &str) -> Result<Self>
@@ -70,7 +112,7 @@ impl Client for TokioClient {
70112

71113
self.execute(&statement, params_refs.as_slice())
72114
.await
73-
.map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))
115+
.map_err(query_failed)
74116
}
75117

76118
async fn query(
@@ -92,7 +134,7 @@ impl Client for TokioClient {
92134
let results = self
93135
.query(&statement, params_refs.as_slice())
94136
.await
95-
.map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?;
137+
.map_err(query_failed)?;
96138

97139
if results.is_empty() {
98140
return Ok(RowSet {
@@ -106,7 +148,7 @@ impl Client for TokioClient {
106148
.iter()
107149
.map(convert_row)
108150
.collect::<Result<Vec<_>, _>>()
109-
.map_err(|e| v4::Error::QueryFailed(format!("{e:?}")))?;
151+
.map_err(|e| v4::Error::QueryFailed(v4::QueryError::Text(format!("{e:?}"))))?;
110152

111153
Ok(RowSet { columns, rows })
112154
}

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/spin-postgres@4.0.0/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)