Skip to content

Commit d1dd4da

Browse files
authored
Support RETURNS TABLE (...) functions (#326)
* working towards supporting RETURNS TABLE. This work was on accident and I'll provide more details in the PR * getting `RETURNS TABLE (...)` working. Needs tests. * - test for `RETURNS TABLE` *docs for SRF functions in general * doc updates * upgrade dependencies, including all previously incompatible ones, and just keep doing that going forward * a test for returning a table with 1 field
1 parent b215975 commit d1dd4da

File tree

14 files changed

+373
-102
lines changed

14 files changed

+373
-102
lines changed

Cargo.lock

Lines changed: 35 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Function Anatomy](./functions/anatomy.md)
1818
- [Arguments](./functions/arguments.md)
1919
- [Return Type](./functions/return-type.md)
20+
- [Set Returning Functions](./functions/set-returning-functions.md)
2021
- [Data types](./data-types.md)
2122
- [No Unsigned Types](./data-types/no-unsigned-types.md)
2223
- [Arrays](./data-types/arrays.md)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Set Returning Functions
2+
3+
PL/Rust supports both set returning function styles, `RETURNS SETOF $type` and `RETURNS TABLE (...)`. In both cases,
4+
the function returns a specialized `Iterator` for the specific style.
5+
6+
It's useful to think of set returning functions as returning something that resembles a table, either with one unnamed
7+
column (`RETURNS SETOF`) or multiple, named columns (`RETURNS TABLE`).
8+
9+
In both cases, the Iterator Item type is an `Option<T>`, where `T` is the [return type](return-type.md). The reason
10+
for this is that PL/Rust needs to allow a returned row/tuple to be NULL (`Option::None`).
11+
12+
## `RETURNS SETOF $type`
13+
14+
`RETURNS SETOF $type` returns a "table" with one, unnamed column. Each returned row must be an `Option` of the return
15+
type, either `Some(T)` or `None`, indicating NULL.
16+
17+
A simple example of splitting a text string on whitespace, following Rust's rules:
18+
19+
```sql
20+
CREATE OR REPLACE FUNCTION split_whitespace(s text) RETURNS SETOF text STRICT LANGUAGE plrust AS $$
21+
let by_whitespace = s.split_whitespace(); // borrows from `s` which is a `&str`
22+
let mapped = by_whitespace.map(|token| {
23+
if token == "this" { None } // just to demonstrate returning a NULL
24+
else { Some(token.to_string()) }
25+
});
26+
let iter = SetOfIterator::new(mapped);
27+
Ok(Some(iter))
28+
$$;
29+
```
30+
31+
PL/Rust generates the following method signature for the above function:
32+
33+
```rust
34+
fn plrust_fn_oid_19691_336344<'a>(
35+
s: &'a str,
36+
) -> ::std::result::Result< // the function itself can return a `Result::Err`
37+
Option< // `Option::None` will return zero rows
38+
::pgrx::iter::SetOfIterator< // indicates returning a set of values
39+
'a, // allows borrowing from `s`
40+
Option<String> // and the type is an optional, owned string
41+
>
42+
>,
43+
Box<dyn std::error::Error + Send + Sync + 'static>, // boilerplate error type
44+
> {
45+
// <your code here>
46+
}
47+
```
48+
49+
And finally, its result:
50+
51+
```sql
52+
SELECT * FROM split_whitespace('hello world, this is a plrust set returning function');
53+
split_whitespace
54+
------------------
55+
hello
56+
world,
57+
-- remember we returned `None` for the token "this"
58+
is
59+
a
60+
plrust
61+
set
62+
returning
63+
function
64+
(9 rows)
65+
```
66+
67+
## `RETURNS TABLE (...)`
68+
69+
Returning a table with multiple named (and typed) columns is similar to returning a set. Instead of `SetOfIterator`,
70+
PL/Rust uses `TableIterator`. `TableIterator` is a Rust `Iterator` whose Item is a tuple where its field types match
71+
those of the UDF being created:
72+
73+
```sql
74+
CREATE OR REPLACE FUNCTION count_words(s text) RETURNS TABLE (count int, word text) STRICT LANGUAGE plrust AS $$
75+
use std::collections::HashMap;
76+
let mut buckets: HashMap<&str, i32> = Default::default();
77+
78+
for word in s.split_whitespace() {
79+
buckets.entry(word).and_modify(|cnt| *cnt += 1).or_insert(1);
80+
}
81+
82+
let as_tuples = buckets.into_iter().map(|(word, cnt)| {
83+
( Some(cnt), Some(word.to_string()) )
84+
});
85+
Ok(Some(TableIterator::new(as_tuples)))
86+
$$;
87+
```
88+
89+
PL/Rust generates this function signature:
90+
91+
```rust
92+
fn plrust_fn_oid_19691_336349<'a>(
93+
s: &'a str,
94+
) -> ::std::result::Result::< // the function itself can return a `Result::Err`
95+
Option< // `Option::None` will return zero rows
96+
::pgrx::iter::TableIterator< // indicates returning a "table" of tuples
97+
'a, // allows borrowing from `s`
98+
( // a Rust tuple
99+
::pgrx::name!(count, Option < i32 >), // the "count" column, can be "NULL" with `Option::None`
100+
::pgrx::name!(word, Option < String >), // the "word" column, can be "NULL" with `Option::None`
101+
),
102+
>,
103+
>,
104+
Box<dyn std::error::Error + Send + Sync + 'static>,
105+
> {
106+
// <your code here>
107+
}
108+
```
109+
110+
And the results from this function are:
111+
112+
```sql
113+
# SELECT * FROM count_words('this is a test that is testing plrust''s SRF support');
114+
count | word
115+
-------+----------
116+
1 | a
117+
1 | test
118+
1 | that
119+
2 | is
120+
1 | this
121+
1 | testing
122+
1 | SRF
123+
1 | support
124+
1 | plrust's
125+
(9 rows)
126+
```
127+
128+
The important thing to keep in mind when writing PL/Rust functions that `RETURNS TABLE` is that the structure being
129+
returned is a Rust tuple of `Option<T>`s where each field's `T` is the [return type](return-type.md) as specified in
130+
the `RETURNS TABLE (...)` clause.

plrust-trusted-pgrx/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pg15 = ["pgrx/pg15"]
1818

1919
[dependencies]
2020
# changing the pgrx version will likely require at least a minor version bump to this create
21-
pgrx = { version = "=0.9.3", features = [ "no-schema-generation" ], default-features = false }
21+
pgrx = { version = "=0.9.4", features = [ "no-schema-generation" ], default-features = false }
2222

2323
[package.metadata.docs.rs]
2424
features = ["pg14"]

plrust-trusted-pgrx/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub use iter::*;
7676
/// Return iterators from plrust functions
7777
pub mod iter {
7878
pub use ::pgrx::iter::{SetOfIterator, TableIterator};
79+
pub use ::pgrx::name;
7980
}
8081

8182
#[doc(hidden)]

plrust/Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ home = "0.5.5" # where can we find cargo?
3535
# working with our entry in pg_catalog.pg_proc
3636
base64 = "0.21.2"
3737
flate2 = "1.0.26"
38-
serde = "1.0.163"
38+
serde = "1.0.164"
3939
serde_json = "1.0.96"
4040

4141
# pgrx core details
42-
pgrx = { version = "=0.9.3" }
42+
pgrx = { version = "=0.9.4" }
4343

4444
# language handler support
45-
libloading = "0.7.4"
45+
libloading = "0.8.0"
4646
toml = "0.7.4"
4747
tempdir = "0.3.7" # for building crates
4848
tempfile = "3.6.0"
@@ -54,10 +54,10 @@ color-eyre = "0.6"
5454
tracing = { version = "0.1", features = [ "valuable" ] }
5555
tracing-subscriber = { version = "0.3", features = [ "env-filter" ] }
5656
tracing-error = "0.2"
57-
prettyplease = "0.1"
57+
prettyplease = "0.2"
5858

5959
# procedural macro handling
60-
syn = "1"
60+
syn = "2"
6161
quote = "1"
6262
proc-macro2 = "1"
6363
omnipath = "0.1.6"
@@ -67,7 +67,7 @@ memfd = "0.6.3" # for anonymously writing/loading user function .so
6767

6868

6969
[dev-dependencies]
70-
pgrx-tests = { version = "=0.9.3" }
70+
pgrx-tests = { version = "=0.9.4" }
7171
tempdir = "0.3.7"
7272
once_cell = "1.18.0"
7373
toml = "0.7.4"

0 commit comments

Comments
 (0)