Skip to content

Commit b200eb7

Browse files
authored
impl IntoDatum for Result<T: IntoDatum, E: Display> (pgcentralfoundation#972)
This pulls out some commits from pgcentralfoundation#969 into its own PR to add support for `#[pg_extern]`-style functions returning a `Result` of some kind. We don't care about the type of the Error, so long as it implements `Display`. If the returned value is the Err variant, the `IntoDatum` impl for `Result<T, E>` will raise a Postgres ERROR using `ereport!(ERROR, PgSqlErrorCode::ERRCODE_DATA_EXCEPTION, &format!("{}", e))`. It's tested to also work with the [`eyre::Result`](https://docs.rs/eyre/latest/eyre/type.Result.html) type along with returning `Result<Option<T>, E>`. The case of returning a `Result<(), E>` uncovered some issues with how pgx converts `()` into a Datum, so those have been fixed along the way. It also allows `#[pg_test]` functions to return Results which makes writing unit tests a little nicer.
1 parent 04b86f8 commit b200eb7

File tree

7 files changed

+230
-12
lines changed

7 files changed

+230
-12
lines changed

pgx-sql-entity-graph/src/metadata/sql_translatable.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ to the `pgx` framework and very subject to change between versions. While you ma
77
88
*/
99
use std::error::Error;
10+
use std::fmt::Display;
1011

1112
use super::return_variant::ReturnsError;
1213
use super::{FunctionMetadataTypeEntity, Returns};
@@ -18,6 +19,7 @@ pub enum ArgumentError {
1819
BareU8,
1920
SkipInArray,
2021
Datum,
22+
NotValidAsArgument(&'static str),
2123
}
2224

2325
impl std::fmt::Display for ArgumentError {
@@ -38,6 +40,9 @@ impl std::fmt::Display for ArgumentError {
3840
ArgumentError::Datum => {
3941
write!(f, "A Datum as an argument means that `sql = \"...\"` must be set in the declaration")
4042
}
43+
ArgumentError::NotValidAsArgument(type_name) => {
44+
write!(f, "`{}` is not able to be used as a function argument", type_name)
45+
}
4146
}
4247
}
4348
}
@@ -102,6 +107,19 @@ pub unsafe trait SqlTranslatable {
102107
}
103108
}
104109

110+
unsafe impl<E> SqlTranslatable for Result<(), E>
111+
where
112+
E: Display,
113+
{
114+
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
115+
Err(ArgumentError::NotValidAsArgument("()"))
116+
}
117+
118+
fn return_sql() -> Result<Returns, ReturnsError> {
119+
Ok(Returns::One(SqlMapping::literal("VOID")))
120+
}
121+
}
122+
105123
unsafe impl<T> SqlTranslatable for Option<T>
106124
where
107125
T: SqlTranslatable,
@@ -135,7 +153,7 @@ where
135153
unsafe impl<T, E> SqlTranslatable for Result<T, E>
136154
where
137155
T: SqlTranslatable,
138-
E: std::error::Error + 'static,
156+
E: Display,
139157
{
140158
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
141159
T::argument_sql()

pgx-sql-entity-graph/src/pg_extern/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,23 @@ impl PgExtern {
419419
quote_spanned! { self.func.sig.output.span() =>
420420
::pgx::fcinfo::pg_return_void()
421421
}
422+
} else if retval_ty.result {
423+
if retval_ty.optional.is_some() {
424+
// returning `Result<Option<T>>`
425+
quote_spanned! {
426+
self.func.sig.output.span() =>
427+
match ::pgx::datum::IntoDatum::into_datum(#result_ident) {
428+
Some(datum) => datum,
429+
None => ::pgx::fcinfo::pg_return_null(#fcinfo_ident),
430+
}
431+
}
432+
} else {
433+
// returning Result<T>
434+
quote_spanned! {
435+
self.func.sig.output.span() =>
436+
::pgx::datum::IntoDatum::into_datum(#result_ident).unwrap_or_else(|| panic!("returned Datum was NULL"))
437+
}
438+
}
422439
} else if retval_ty.resolved_ty == syn::parse_quote!(pg_sys::Datum)
423440
|| retval_ty.resolved_ty == syn::parse_quote!(pgx::pg_sys::Datum)
424441
|| retval_ty.resolved_ty == syn::parse_quote!(::pgx::pg_sys::Datum)

pgx-sql-entity-graph/src/used_type.rs

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ pub struct UsedType {
2727
/// Set via `VariadicArray` or `variadic!()`
2828
pub variadic: bool,
2929
pub default: Option<String>,
30-
/// Set via the type being an `Option`.
30+
/// Set via the type being an `Option` or a `Result<Option<T>>`.
3131
pub optional: Option<syn::Type>,
32+
/// Set via the type being a `Result<T>`
33+
pub result: bool,
3234
}
3335

3436
impl UsedType {
@@ -131,8 +133,8 @@ impl UsedType {
131133
original => (original, None),
132134
};
133135

134-
// In this step, we go look at the resolved type and determine if it is a variadic, optional, etc.
135-
let (resolved_ty, variadic, optional) = match resolved_ty {
136+
// In this step, we go look at the resolved type and determine if it is a variadic, optional, result, etc.
137+
let (resolved_ty, variadic, optional, result) = match resolved_ty {
136138
syn::Type::Path(type_path) => {
137139
let path = &type_path.path;
138140
let last_segment = path.segments.last().ok_or(syn::Error::new(
@@ -141,6 +143,68 @@ impl UsedType {
141143
))?;
142144
let ident_string = last_segment.ident.to_string();
143145
match ident_string.as_str() {
146+
"Result" => {
147+
match &last_segment.arguments {
148+
syn::PathArguments::AngleBracketed(angle_bracketed) => {
149+
match angle_bracketed.args.first().ok_or(syn::Error::new(
150+
angle_bracketed.span(),
151+
"No inner arg for Result<T, E> found",
152+
))? {
153+
syn::GenericArgument::Type(inner_ty) => {
154+
match inner_ty {
155+
// Result<$Type<T>>
156+
syn::Type::Path(ref inner_type_path) => {
157+
let path = &inner_type_path.path;
158+
let last_segment =
159+
path.segments.last().ok_or(syn::Error::new(
160+
path.span(),
161+
"No last segment found while scanning path",
162+
))?;
163+
let ident_string = last_segment.ident.to_string();
164+
match ident_string.as_str() {
165+
"VariadicArray" => (
166+
syn::Type::Path(type_path.clone()),
167+
true,
168+
Some(inner_ty.clone()),
169+
false,
170+
),
171+
"Option" => (
172+
syn::Type::Path(type_path.clone()),
173+
false,
174+
Some(inner_ty.clone()),
175+
true,
176+
),
177+
_ => (
178+
syn::Type::Path(type_path.clone()),
179+
false,
180+
None,
181+
true,
182+
),
183+
}
184+
}
185+
// Result<T>
186+
_ => (
187+
syn::Type::Path(type_path.clone()),
188+
false,
189+
None,
190+
true,
191+
),
192+
}
193+
}
194+
_ => {
195+
return Err(syn::Error::new(
196+
type_path.span().clone(),
197+
"Unexpected Item found inside `Result` (expected Type)",
198+
))
199+
}
200+
}
201+
}
202+
_ => return Err(syn::Error::new(
203+
type_path.span().clone(),
204+
"Unexpected Item found inside `Result` (expected Angle Brackets)",
205+
)),
206+
}
207+
}
144208
"Option" => {
145209
// Option<VariadicArray<T>>
146210
match &last_segment.arguments {
@@ -166,11 +230,13 @@ impl UsedType {
166230
syn::Type::Path(type_path.clone()),
167231
true,
168232
Some(inner_ty.clone()),
233+
false,
169234
),
170235
_ => (
171236
syn::Type::Path(type_path.clone()),
172237
false,
173238
Some(inner_ty.clone()),
239+
false,
174240
),
175241
}
176242
}
@@ -179,6 +245,7 @@ impl UsedType {
179245
syn::Type::Path(type_path.clone()),
180246
false,
181247
Some(inner_ty.clone()),
248+
false,
182249
),
183250
}
184251
}
@@ -199,15 +266,15 @@ impl UsedType {
199266
}
200267
}
201268
// VariadicArray<T>
202-
"VariadicArray" => (syn::Type::Path(type_path), true, None),
269+
"VariadicArray" => (syn::Type::Path(type_path), true, None, false),
203270
// T
204-
_ => (syn::Type::Path(type_path), false, None),
271+
_ => (syn::Type::Path(type_path), false, None, false),
205272
}
206273
}
207-
original => (original, false, None),
274+
original => (original, false, None, false),
208275
};
209276

210-
Ok(Self { original_ty, resolved_ty, optional, variadic, default, composite_type })
277+
Ok(Self { original_ty, resolved_ty, optional, result, variadic, default, composite_type })
211278
}
212279

213280
pub fn entity_tokens(&self) -> syn::Expr {

pgx-tests/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ time = "0.3.17"
4848
eyre = "0.6.8"
4949
thiserror = "1.0"
5050

51+
[dev-dependencies]
52+
eyre = "0.6.8" # testing functions that return `eyre::Result`
53+
5154
[dependencies.pgx]
5255
path = "../pgx"
5356
default-features = false

pgx-tests/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod pgbox_tests;
3838
mod pgx_module_qualification;
3939
mod postgres_type_tests;
4040
mod range_tests;
41+
mod result_tests;
4142
mod schema_tests;
4243
mod shmem_tests;
4344
mod spi_tests;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#[cfg(any(test, feature = "pg_test"))]
2+
#[pgx::pg_schema]
3+
mod tests {
4+
#[allow(unused_imports)]
5+
use crate as pgx_tests;
6+
use pgx::prelude::*;
7+
use std::convert::Infallible;
8+
9+
#[pg_extern]
10+
fn return_result() -> Result<i32, Infallible> {
11+
Ok(42)
12+
}
13+
14+
#[pg_extern]
15+
fn return_some_optional_result() -> Result<Option<i32>, Infallible> {
16+
Ok(Some(42))
17+
}
18+
19+
#[pg_extern]
20+
fn return_none_optional_result() -> Result<Option<i32>, Infallible> {
21+
Ok(None)
22+
}
23+
24+
#[pg_extern]
25+
fn return_some_error() -> Result<i32, Box<dyn std::error::Error>> {
26+
Err("error!".into())
27+
}
28+
29+
#[pg_extern]
30+
fn return_eyre_result() -> eyre::Result<i32> {
31+
Ok(42)
32+
}
33+
34+
#[pg_extern]
35+
fn return_eyre_result_error() -> eyre::Result<i32> {
36+
Err(eyre::eyre!("error!"))
37+
}
38+
39+
#[pg_test(error = "No such file or directory (os error 2)")]
40+
fn test_return_io_error() -> Result<(), std::io::Error> {
41+
std::fs::read("/tmp/i-sure-hope-this-doest-exist.pgx-tests::test_result_result").map(|_| ())
42+
}
43+
44+
#[pg_test]
45+
fn test_return_result() {
46+
Spi::get_one::<i32>("SELECT tests.return_result()").expect("SPI returned NULL");
47+
}
48+
49+
#[pg_test]
50+
fn test_return_some_optional_result() {
51+
assert_eq!(Some(42), Spi::get_one::<i32>("SELECT tests.return_some_optional_result()"));
52+
}
53+
54+
#[pg_test]
55+
fn test_return_none_optional_result() {
56+
assert_eq!(None, Spi::get_one::<i32>("SELECT tests.return_none_optional_result()"));
57+
}
58+
59+
#[pg_test(error = "error!")]
60+
fn test_return_some_error() {
61+
Spi::get_one::<i32>("SELECT tests.return_some_error()").expect("SPI returned NULL");
62+
}
63+
64+
#[pg_test]
65+
fn test_return_eyre_result() {
66+
Spi::get_one::<i32>("SELECT tests.return_eyre_result()").expect("SPI returned NULL");
67+
}
68+
69+
#[pg_test(error = "error!")]
70+
fn test_return_eyre_result_error() {
71+
Spi::get_one::<i32>("SELECT tests.return_eyre_result_error()").expect("SPI returned NULL");
72+
}
73+
74+
#[pg_test(error = "got proper sql errorcode")]
75+
fn test_proper_sql_errcode() -> Option<i32> {
76+
PgTryBuilder::new(|| Spi::get_one::<i32>("SELECT tests.return_eyre_result_error()"))
77+
.catch_when(PgSqlErrorCode::ERRCODE_DATA_EXCEPTION, |_| {
78+
panic!("got proper sql errorcode")
79+
})
80+
.execute()
81+
}
82+
}

pgx/src/datum/into.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ Use of this source code is governed by the MIT license that can be found in the
1313
//! cast of the primitive type to pg_sys::Datum
1414
1515
use crate::{pg_sys, rust_regtypein, PgBox, PgOid, WhoAllocated};
16-
use pgx_pg_sys::{Datum, Oid};
16+
use core::fmt::Display;
17+
use pgx_pg_sys::errcodes::PgSqlErrorCode;
18+
use pgx_pg_sys::{ereport, Datum, Oid};
1719

1820
/// Convert a Rust type into a `pg_sys::Datum`.
1921
///
@@ -120,6 +122,33 @@ where
120122
}
121123
}
122124

125+
impl<T, E> IntoDatum for Result<T, E>
126+
where
127+
T: IntoDatum,
128+
E: Display,
129+
{
130+
/// Returns The `Option<pg_sys::Datum>` representation of this Result's `Ok` variant.
131+
///
132+
/// ## Panics
133+
///
134+
/// If this Result represents an error, then that error is raised as a Postgres ERROR, using
135+
/// the [`PgSqlErrorCode::ERRCODE_DATA_EXCEPTION`] error code
136+
#[inline]
137+
fn into_datum(self) -> Option<Datum> {
138+
match self {
139+
Ok(v) => v.into_datum(),
140+
Err(e) => {
141+
ereport!(ERROR, PgSqlErrorCode::ERRCODE_DATA_EXCEPTION, &format!("{}", e));
142+
}
143+
}
144+
}
145+
146+
#[inline]
147+
fn type_oid() -> pg_sys::Oid {
148+
T::type_oid()
149+
}
150+
}
151+
123152
/// for bool
124153
impl IntoDatum for bool {
125154
#[inline]
@@ -380,15 +409,16 @@ impl IntoDatum for Vec<u8> {
380409
}
381410
}
382411

383-
/// for NULL -- always converts to `None`
412+
/// for VOID
384413
impl IntoDatum for () {
385414
#[inline]
386415
fn into_datum(self) -> Option<pg_sys::Datum> {
387-
None
416+
// VOID isn't very useful, but Postgres represents it as a non-null Datum with a zero value
417+
Some(Datum::from(0))
388418
}
389419

390420
fn type_oid() -> u32 {
391-
pg_sys::BOOLOID
421+
pg_sys::VOIDOID
392422
}
393423
}
394424

0 commit comments

Comments
 (0)