Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RetAbi and BoxRet #1701

Merged

Conversation

workingjubilee
Copy link
Member

@workingjubilee workingjubilee commented May 14, 2024

This completely reworks the way we handle returning types from functions, so that we no longer have to rely on a macro expansion behavior that has to already know the types at expansion time (and thus has to parse them somehow, which it cannot realistically do because type aliases exist). Instead, we simply expand into code that asks the types themselves to modify the FunctionCallInfo appropriately and then return a raw Datum to Postgres.

This breaks support for certain returns because it was difficult to do this and also support arbitrary nesting, because Postgres does not support arbitrary nesting. For instance, you can no longer return:

  • SetOfIterator<'a, Result<T, E>>
  • TableIterator<'a, (Result<T, E>, Result<U, D>)>
  • Option<TableIterator<'a, Tuple>>

It's expected that this will improve in the near-ish future.

This also breaks returning values from #[pg_extern] functions that were relying on IntoDatum implementations being enough. It is not expected this will improve, for the reasons described on the documentation of the new traits, which can be summarized as "IntoDatum should never have been used for that bound". This change blocks off several latent correctness problems from affecting pgrx going forward.

Fixes #1484

pgrx/src/callconv.rs Outdated Show resolved Hide resolved
pgrx/src/callconv.rs Outdated Show resolved Hide resolved
pgrx/src/iter.rs Outdated Show resolved Hide resolved
pgrx/src/iter.rs Outdated Show resolved Hide resolved
pgrx/src/iter.rs Outdated Show resolved Hide resolved
pgrx/src/iter.rs Outdated Show resolved Hide resolved
Copy link
Member Author

@workingjubilee workingjubilee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the iterator conveniences are basically independent, I'll rebase this on #1703 after that lands.

Comment on lines +283 to +297
) -> Result<TableIterator<'static, (name!(id, i32), name!(relname, Option<String>))>, spi::Error>
{
let oids = vec![1213, 1214, 1232, 1233, 1247, 1249, 1255];

TableIterator::new(oids.into_iter().map(|oid| {
(oid, Spi::get_one(&format!("SELECT CAUSE_AN_ERROR FROM pg_class WHERE oid = {oid}")))
}))
let result = oids
.into_iter()
.map(|oid| {
Ok((
oid,
Spi::get_one(&format!(
"SELECT CAUSE_AN_ERROR FROM pg_class WHERE oid = {oid}"
))?,
))
})
.collect::<Result<Vec<_>, _>>();
result.map(|v| TableIterator::new(v))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this should be TableIterator<'_, Result<Tuple, Error>> so that no eager evaluation has to happen. I think I can make that work with a bit more effort. But the schema generator chokes on that! And that is another reason why I want it out of our way.

I do not think people should be required to return TableIterator<'a, (Result, Result, Result, Result, Result, Result)> if they want to fallibly return a row, but they currently are. And I do not think this should be blocked on that.

Asking people to propagate errors to the top of each row return should be fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asking people to propagate errors to the top of each row return should be fine.

I'm not sure this is a nice thing to ask. When SPI, for example, is involved, and what you're trying to do is return a TableIterator of a bunch of individual values you somehow got from SPI, it's definitely a lot easier to just return the Result<T> (or Result<Option<T>>) in whatever tuple position. Especially if there's quite a few values in the tuple.

This isn't a hill I'm willing to die on, but it'd be nice to support, for example, TableIterator<(Result<S>, T, Result<U>, Result<V>, W, Result<X>, Y, Result<Z>)>. Then we'll panic whenever we get to unwrapping that result and the user doesn't have to worry about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eeeebbbbrrrr Honestly, we'll never have good UX until we expand to supporting actual struct arguments, really.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "actual struct arguments" mean?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TableIterator should be able to return a struct that describes the row in question, but this requires being able to use traits like these to return fairly arbitrary generic types. It doesn't work as long as we rely on parsing.

pgrx-examples/spi_srf/src/lib.rs Show resolved Hide resolved
@workingjubilee workingjubilee marked this pull request as ready for review May 15, 2024 05:23
@workingjubilee
Copy link
Member Author

This PR opens a LOT of pure simplifications like 9dd8f5f, it's just going to take forever to find them all.

.collect::<Vec<_>>();
Ok(TableIterator::new(filtered))
.map(|row| {
Ok((row["dog_name"].value()?, row["dog_age"].value()?, row["dog_breed"].value()?))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eeeebbbbrrrr it's certainly not less code but in most cases it just looks like going "hello? I? have? a? question? if that's okay?"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not terrible, but I'm sure you agree that returning a Result from a closure is awkward at best. In this case we .collect::<Result<Vec<_>>>() so it's not too terrible.

I prefer the left side of this diff, but like I said elsewhere, I'm not willing to die on this hill.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the left side is a bit nicer, but that type signature... that's what I really have it in for. so yeah, if I didn't believe it would open up better possibilities in short order, I wouldn't do it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what I really have it in for

Yeah, that's why I'm not really gonna fight on this one. I hate typing out all that mess too. But once the return signature has been typed out, it makes it a lot easier to just write some code and return some values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many question marks is much-preferable to having excessively-verbose nested type signatures, yeah.

    tests::heap_tuple::tests::pg_test_tuple_desc_clone
    tests::result_tests::tests::pg_test_return_result_set_of
    tests::result_tests::tests::pg_test_return_result_set_of_error
    tests::srf_tests::tests::pg_test_generate_series
    tests::srf_tests::tests::pg_test_return_none_setof_iterator
    tests::srf_tests::tests::pg_test_return_some_setof_iterator
    tests::srf_tests::tests::pg_test_srf_setof_datum_detoasting_with_borrow
    tests::struct_type_tests::tests::pg_test_complex_storage_and_retrieval
They now know how to create their own tupdesc in the context.
Comment on lines +301 to +309
pub fn spi_in_setof() -> Result<SetOfIterator<'static, Option<String>>, spi::Error> {
let oids = vec![1213, 1214, 1232, 1233, 1247, 1249, 1255];

SetOfIterator::new(oids.into_iter().map(|oid| {
Spi::get_one(&format!("SELECT CAUSE_AN_ERROR FROM pg_class WHERE oid = {oid}"))
}))
let result = oids
.into_iter()
.map(|oid| {
Spi::get_one(&format!("SELECT CAUSE_AN_ERROR FROM pg_class WHERE oid = {oid}"))
})
.collect::<Result<Vec<Option<_>>, _>>();
result.map(SetOfIterator::new)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking this one annoys me much more than the TableIterator nesting case but I want to make sure that SetOfIterator has exactly as stringent requirements, currently, as TableIterator<'a, (T,)>, because it will make things much worse down the line if they meaningfully differ.

Copy link
Contributor

@NotGyro NotGyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. This really does make code working with the returns from Rust to Postgres much cleaner and more elegant.

I had some confusion about the SetOfIterator type, but it was cleared up in a company meeting. The single-item case behaves differently than the iterator case for legacy Postgres reasons, which is why SetOfIterator can be one item and/or an iterator.

Copy link
Contributor

@NotGyro NotGyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the documentation! This explains it neatly.

This allows hiding the details of the type so it cannot be constructed
nor meddled with externally, and simplifies many actual cases.
@workingjubilee
Copy link
Member Author

This last commit makes the "Ret" type into an associated type and thus makes the iterators eat all the complexity of their return ABI as now only they have to know about how to drive themselves forward. Result does not have to think about them, and the implementation details are once more private to iterators and fully hidden behind the trait. This prevents anyone from trying to make any of the many bug-inducing nonsensical calls that were technically allowed at first, and probably made the code have to carry around way more panic branches.

@workingjubilee
Copy link
Member Author

The last few commits were just cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove TableIterator-to-SetOfIterator remapping codegen?
3 participants