Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
fc7435d
Make composite_row_type_make_tuple unsafe and fully detoast its row
eeeebbbbrrrr Oct 28, 2026
07eebcb
Lend PgVarlena references from an aligned copy of the payload
eeeebbbbrrrr Oct 28, 2026
29922d7
Accept SPI_OK_MERGE_RETURNING on PG17 and later
eeeebbbbrrrr Oct 28, 2026
b5a5321
Skip LWLockRelease while unwinding a PostgreSQL ERROR in subtrans_infos
eeeebbbbrrrr Oct 28, 2026
9d36b4e
Keep PgVarlena's value in Rust and allocate its varlena in into_pg
eeeebbbbrrrr Oct 28, 2026
6c2211d
Require Default for PgVarlena::new and add new_zeroed and From<T>
eeeebbbbrrrr Oct 28, 2026
55f4e04
Make heap_getattr and heap_getattr_datum_ex unsafe
eeeebbbbrrrr Oct 28, 2026
040d592
Stop holding an attribute reference across heap_getattr_raw in get_by…
eeeebbbbrrrr Oct 28, 2026
99422e1
Test the subtrans_infos lock guard across a PostgreSQL ERROR
eeeebbbbrrrr Oct 28, 2026
ea47718
Keep PgVarlena padding zero after new_zeroed and document padding
eeeebbbbrrrr Oct 28, 2026
df5246c
Look up the Rust type only for non-NULL attributes in get_by_index
eeeebbbbrrrr Oct 28, 2026
6a154f5
Describe the tuples heap_getattr accepts as PostgreSQL defines them
eeeebbbbrrrr Oct 28, 2026
c379f4f
State the heap_getattr tuple contract by stored layout and descriptor…
eeeebbbbrrrr Oct 28, 2026
a7f2e7f
Say that PgVarlena's Default never guaranteed zero padding
eeeebbbbrrrr Oct 28, 2026
f24c7d7
Check the attribute type when get_by_index reads a record
eeeebbbbrrrr Oct 28, 2026
0266565
Make DatumWithTypeInfo::into_value unsafe
eeeebbbbrrrr Oct 28, 2026
01f908f
Take heap_getattr_datum_ex's length and by-value flag from the descri…
eeeebbbbrrrr Oct 28, 2026
f00db30
Read a dropped attribute as NULL in PgHeapTuple::get_by_index
eeeebbbbrrrr Oct 28, 2026
08f3441
Reject a dropped attribute in PgHeapTuple::set_by_index
eeeebbbbrrrr Oct 28, 2026
ebe50fc
List the remaining PgHeapTuple and PgVarlena behavior changes in the …
eeeebbbbrrrr Oct 28, 2026
1844b29
Return copies of tuple descriptor attributes
eeeebbbbrrrr Oct 28, 2026
28962f5
Return IncompatibleTypes for an invalid or unknown type OID
eeeebbbbrrrr Oct 28, 2026
93db6e3
Make AnyArray::into unsafe and add checked AnyArray::value and AnyEle…
eeeebbbbrrrr Oct 28, 2026
da8652c
Keep AnyArray elements valid after the iterator drops
eeeebbbbrrrr Oct 28, 2026
cb8a427
Copy AnyElement and AnyArray values out of SPI results
eeeebbbbrrrr Oct 28, 2026
6e8b6da
Give elements of a domain-over-array AnyArray their element type
eeeebbbbrrrr Oct 28, 2026
51740ea
Describe the tuple descriptor and AnyArray changes in the upgrade note
eeeebbbbrrrr Oct 28, 2026
4cde7ca
Copy PgBox array elements when unboxing them
eeeebbbbrrrr Oct 28, 2026
0f2c946
Remove PgTupleDesc's Deref to the descriptor header
eeeebbbbrrrr Oct 28, 2026
73510af
Return copies from PgRelation instead of references into PostgreSQL m…
eeeebbbbrrrr Oct 28, 2026
f542245
Copy PgBox tuple attributes when reading them
eeeebbbbrrrr Oct 28, 2026
3729fc0
Look up Rust-named types without search_path
eeeebbbbrrrr Oct 28, 2026
a9d5341
Check text array elements for UTF-8
eeeebbbbrrrr Oct 28, 2026
7b71385
Say how long from_datum_in_memory_context's context must live
eeeebbbbrrrr Oct 28, 2026
2a92383
Bind borrowed PostgresType values to their Datum's lifetime
eeeebbbbrrrr Oct 28, 2026
d756685
Make generated binary receive functions unsafe
eeeebbbbrrrr Oct 28, 2026
501c84b
Check fn_call argument types against the function's parameters
eeeebbbbrrrr Oct 28, 2026
9e7e97f
Allocate PgVarlena varlenas at their exact size
eeeebbbbrrrr Oct 29, 2026
0db1e08
State what PgVarlena::from_datum requires of its varlena
eeeebbbbrrrr Oct 29, 2026
3c2ff3e
Test PgVarlena reads of table rows and toasted values
eeeebbbbrrrr Oct 29, 2026
e99b39f
Describe the PgVarlena example as copying its payload
eeeebbbbrrrr Oct 29, 2026
5440254
Reject shell types in the extension type lookup
eeeebbbbrrrr Oct 29, 2026
dfc03eb
Stop probing pg_depend once per same-named type in every schema
eeeebbbbrrrr Oct 29, 2026
65d293d
Count .control files the same way in pg_module_magic and cargo pgrx
eeeebbbbrrrr Oct 29, 2026
1d8d43f
Document rust_regtypein's failure modes, cost and foreign types
eeeebbbbrrrr Oct 29, 2026
64c2fcc
Run the PgVarlena padding test under Miri in CI
eeeebbbbrrrr Oct 29, 2026
2eeb1bd
Skip row and array types that PostgreSQL makes in the extension type …
eeeebbbbrrrr Oct 29, 2026
6bf37d5
Reject PgBox reads of types that a size_of::<T>() copy cannot hold
eeeebbbbrrrr Oct 28, 2026
8d8ef00
Read an xid Datum's low 32 bits, as DatumGetTransactionId does
eeeebbbbrrrr Oct 28, 2026
359c24d
Widen narrow by-value array elements as fetch_att does
eeeebbbbrrrr Oct 28, 2026
bccd9e5
Detoast an AnyArray once when the handle is made
eeeebbbbrrrr Oct 28, 2026
f06ca0b
Look up an array's element type once per array
eeeebbbbrrrr Oct 28, 2026
54bd62d
State the UnboxDatum and POINTS_INTO_DATUM rules that pgrx's own type…
eeeebbbbrrrr Oct 28, 2026
7e98975
Say that SPI keeps copied values until the transaction ends
eeeebbbbrrrr Oct 28, 2026
0b6352f
Check SPI's AnyElement and AnyArray copies without relying on freed-m…
eeeebbbbrrrr Oct 28, 2026
29abd68
Test which layouts PgBox<T> can unbox
eeeebbbbrrrr Oct 29, 2026
e5862aa
Note the xid read fix in the upgrade note
eeeebbbbrrrr Oct 29, 2026
23c6cc6
Test the SPI AnyArray copy with an array that keeps a 4-byte header
eeeebbbbrrrr Oct 29, 2026
0dbcdab
Say which raw array element Datums now match fetch_att
eeeebbbbrrrr Oct 29, 2026
76a1524
Say where an AnyArray's detoasted copy lives
eeeebbbbrrrr Oct 29, 2026
6c83f7d
Document that value() panics for PgBox elements it cannot copy
eeeebbbbrrrr Oct 29, 2026
2f6028b
Name the over-aligned test struct's field to silence dead_code
eeeebbbbrrrr Oct 29, 2026
3ff60a2
Say that borrowed tuple values can point into CurrentMemoryContext
eeeebbbbrrrr Oct 28, 2026
f0386c5
Check for NULL before PgTupleDesc::from_pg_copy reads the descriptor
eeeebbbbrrrr Oct 28, 2026
44233a8
State the preconditions of PgRelation's open and close and of heap_ge…
eeeebbbbrrrr Oct 28, 2026
48d7329
Correct stale PgRelation, fn_call and wal_decoder documentation
eeeebbbbrrrr Oct 28, 2026
5b158ad
Make the NULL attribute test fail if get_by_index looks up the type f…
eeeebbbbrrrr Oct 28, 2026
6c86ed1
Look up each fn_call argument type and get_by_index type once per call
eeeebbbbrrrr Oct 28, 2026
635e430
Make the subtrans_infos guard test fail without assertions
eeeebbbbrrrr Oct 28, 2026
bfddd52
Make the PgRelation::namespace test fail without freed-memory clobbering
eeeebbbbrrrr Oct 28, 2026
99d3bf5
Read get_by_index values through T's try_ conversions again
eeeebbbbrrrr Oct 29, 2026
c585993
Look up T's type once when set_by_index checks it
eeeebbbbrrrr Oct 29, 2026
2d350e3
Say that get_by_index's PgBox values must not outlive CurrentMemoryCo…
eeeebbbbrrrr Oct 29, 2026
05ce506
Correct the wal_decoder example's SAFETY comments
eeeebbbbrrrr Oct 29, 2026
4935e31
Test fn_call's VARIADIC and set-returning function limits
eeeebbbbrrrr Oct 29, 2026
640ad46
Say which attributes get_by_index can read as PgBox<T>
eeeebbbbrrrr Oct 29, 2026
8ef2915
Raise the shell-type ERROR for pg_catalog types too
eeeebbbbrrrr Oct 29, 2026
00d4a73
Skip multirange types in the extension type lookup
eeeebbbbrrrr Oct 29, 2026
8f75292
Test the member listing's skip of an extension table's row type
eeeebbbbrrrr Oct 29, 2026
21542c0
Test a NULL row-type attribute in the get_by_index NULL test
eeeebbbbrrrr Oct 29, 2026
2b612a1
Say that a PgTupleDesc copy must not outlive its memory context
eeeebbbbrrrr Oct 29, 2026
28502e5
Name working readers for PgBox array elements in the upgrade note
eeeebbbbrrrr Oct 29, 2026
fe27d6a
Say which text array elements panic in each database encoding
eeeebbbbrrrr Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,12 @@ jobs:
--test macro_build --test shipped_macros -- --include-ignored --test-threads=1

# Only Miri checks that a pointer Datum keeps its provenance through an integer, and its
# strict provenance mode makes the fcinfo test reject an integer round trip. Miri does
# not support RUSTC_WRAPPER, so this step runs without sccache. The nightly is pinned so
# that a new nightly, or one shipped without Miri, cannot fail this step on its own.
- name: Check Datum pointer provenance with Miri
# strict provenance mode makes the fcinfo test reject an integer round trip. In the varlena
# tests, Miri also reports padding that a typed copy left uninitialized, where a native
# run reads whatever the stack held. Miri does not support RUSTC_WRAPPER, so this
# step runs without sccache. The nightly is pinned so that a new nightly, or one shipped
# without Miri, cannot fail this step on its own.
- name: Check pointer provenance and PgVarlena padding with Miri
if: matrix.postgres == 18 && matrix.rust == null
env:
MIRI_TOOLCHAIN: nightly-2026-10-29
Expand All @@ -299,6 +301,8 @@ jobs:
--features "pg$PG_VER" --no-default-features --lib datum
MIRIFLAGS=-Zmiri-strict-provenance cargo "+$MIRI_TOOLCHAIN" miri test --package pgrx \
--features "pg$PG_VER" --no-default-features --lib fcinfo
MIRIFLAGS=-Zmiri-strict-provenance cargo "+$MIRI_TOOLCHAIN" miri test --package pgrx \
--features "pg$PG_VER" --no-default-features --lib varlena

# Only this step sees the target headers; tests above check that builds without them refuse.
- name: Check that cross-compiled pgrx-unit-tests can build
Expand Down
2 changes: 2 additions & 0 deletions cargo-pgrx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ Cargo.toml example.control sql src

`cargo pgrx new <extname>` is an easy way to get started creating a new extension. It's similar to `cargo new <name>`, but does the additional things necessary to support building a Rust Postgres extension.

The extension's name is the name of the one `.control` file next to `Cargo.toml`. `cargo pgrx` ignores files whose names start with `.`, such as editor lock files, and stops with an error if it finds more than one `.control` file.

If you'd like to create a "background worker" instead, specify the `--bgworker` argument.

`cargo pgrx new` does not initialize the directory as a git repo, but it does create a `.gitignore` file in case you decide to do so.
Expand Down
80 changes: 60 additions & 20 deletions cargo-pgrx/src/command/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,46 @@ pub fn get_property(manifest_path: &Path, name: &str) -> eyre::Result<Option<Str
Ok(None)
}

/// Find the crate's one `.control` file, whose stem names the extension. This counts files as
/// `pg_module_magic!` does when it records the extension that `rust_regtypein` looks types up in:
/// names that start with `.`, such as editor lock files, and entries that are not files or links
/// to files do not count.
pub(crate) fn find_control_file(manifest_path: &Path) -> eyre::Result<(PathBuf, String)> {
let parent = manifest_path
.parent()
.ok_or_else(|| eyre!("could not get parent of `{}`", manifest_path.display()))?;

for f in (std::fs::read_dir(parent).wrap_err_with(|| {
eyre!("cannot open current directory `{}` for reading", parent.display())
})?)
.flatten()
{
let f_path = f.path();
if f_path.extension() == Some("control".as_ref()) {
let file_stem = f_path
.file_stem()
.ok_or_else(|| eyre!("could not get file stem of `{}`", f_path.display()))?;
let file_stem = file_stem
.to_str()
.ok_or_else(|| {
eyre!("could not get file stem as String from `{}`", f_path.display())
})?
.to_string();
return Ok((f_path, file_stem));
let controls = std::fs::read_dir(parent)
.wrap_err_with(|| {
eyre!("cannot open current directory `{}` for reading", parent.display())
})?
.flatten()
.map(|f| f.path())
.filter(|path| {
path.extension() == Some("control".as_ref())
&& !path.file_name().is_some_and(|name| name.as_encoded_bytes().starts_with(b"."))
&& std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
})
.collect::<Vec<_>>();
let f_path = match controls.as_slice() {
[] => return Err(eyre!("control file not found in `{}`", manifest_path.display())),
[control] => control.clone(),
_ => {
return Err(eyre!(
"found {} control files in `{}`; an extension crate needs exactly one",
controls.len(),
parent.display()
));
}
}

Err(eyre!("control file not found in `{}`", manifest_path.display()))
};
let file_stem = f_path
.file_stem()
.ok_or_else(|| eyre!("could not get file stem of `{}`", f_path.display()))?;
let file_stem = file_stem
.to_str()
.ok_or_else(|| eyre!("could not get file stem as String from `{}`", f_path.display()))?
.to_string();
Ok((f_path, file_stem))
}

fn determine_git_hash() -> eyre::Result<Option<String>> {
Expand All @@ -134,3 +148,29 @@ fn determine_git_hash() -> eyre::Result<Option<String>> {
Err(e) => Err(e).wrap_err("problem running `git` to determine the current revision hash"),
}
}

#[cfg(test)]
mod tests {
use super::find_control_file;

#[test]
#[cfg(unix)]
fn find_control_file_counts_one_visible_file() {
use std::os::unix::fs::symlink;

let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
assert!(find_control_file(&manifest).is_err());

std::fs::write(dir.path().join("ext.control"), "").unwrap();
// An Emacs lock file, a link to a file that does not exist
symlink("user@host.1234:5678", dir.path().join(".#ext.control")).unwrap();
symlink("missing.control", dir.path().join("dangling.control")).unwrap();
let (path, name) = find_control_file(&manifest).unwrap();
assert_eq!((path, name.as_str()), (dir.path().join("ext.control"), "ext"));

std::fs::write(dir.path().join("ext_old.control"), "").unwrap();
let error = find_control_file(&manifest).unwrap_err().to_string();
assert!(error.starts_with("found 2 control files"), "{error}");
}
}
12 changes: 6 additions & 6 deletions pgrx-examples/custom_types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ impl PgVarlenaInOutFuncs for MyType {
let mut iter = input.to_str().unwrap().split(',');
let (a, b, c) = (iter.next(), iter.next(), iter.next());

let mut result = PgVarlena::<MyType>::new();
result.a = f32::from_str(a.unwrap()).expect("a is not a valid f32");
result.b = f32::from_str(b.unwrap()).expect("b is not a valid f32");
result.c = i64::from_str(c.unwrap()).expect("c is not a valid i64");
result
PgVarlena::from(MyType {
a: f32::from_str(a.unwrap()).expect("a is not a valid f32"),
b: f32::from_str(b.unwrap()).expect("b is not a valid f32"),
c: i64::from_str(c.unwrap()).expect("c is not a valid i64"),
})
}

// Output ourselves as text into the provided `StringInfo` buffer
Expand All @@ -105,7 +105,7 @@ impl PgVarlenaInOutFuncs for MyType {

#[pg_extern]
fn do_a_thing(mut input: PgVarlena<MyType>) -> PgVarlena<MyType> {
input.c += 99; // performs a copy-on-write on the backing varlena pointer
input.c += 99; // changes the Rust copy; the returned Datum is a new varlena
input
}
```
Expand Down
8 changes: 4 additions & 4 deletions pgrx-examples/custom_types/src/fixed_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ pub struct FixedF32Array {

impl PgVarlenaInOutFuncs for FixedF32Array {
fn input(input: &CStr) -> PgVarlena<Self> {
let mut result = PgVarlena::<Self>::new();
let mut result = FixedF32Array { array: [0.0; 91] };

for (i, value) in input.to_bytes().split(|b| *b == b',').enumerate() {
result.array[i] =
f32::from_str(unsafe { std::str::from_utf8_unchecked(value) }).expect("invalid f32")
}

result
PgVarlena::from(result)
}

fn output(&self, buffer: &mut StringInfo) {
Expand All @@ -53,13 +53,13 @@ fn fixedf32array_add(
left: PgVarlena<FixedF32Array>,
right: PgVarlena<FixedF32Array>,
) -> PgVarlena<FixedF32Array> {
let mut new = PgVarlena::<FixedF32Array>::new();
let mut new = FixedF32Array { array: [0.0; 91] };
left.array
.iter()
.zip(right.array.iter())
.map(|(a, b)| a + b)
.zip(new.array.iter_mut())
.for_each(|(a, b)| *b = a);

new
PgVarlena::from(new)
}
20 changes: 12 additions & 8 deletions pgrx-examples/operators/src/pgvarlena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@ pub struct PgVarlenaThing {
b: u64,
c: i32,
d: [u8; 5],
/// `PgVarlena` stores every byte of the value, and a padding byte's value is unspecified.
/// This field fills the 7 bytes that alignment would otherwise leave as padding, so the
/// stored bytes are always the same for equal values.
_pad: [u8; 7],
}

// The fields cover every byte, so the type has no padding.
const _: () = assert!(size_of::<PgVarlenaThing>() == 8 + 8 + 4 + 5 + 7);

impl PgVarlenaThing {
#[allow(dead_code)]
pub fn new(a: u64, b: u64, c: i32, d: [u8; 5]) -> Self {
Self { a, b, c, d }
Self { a, b, c, d, _pad: [0; 7] }
}
}

Expand All @@ -70,21 +76,19 @@ impl PgVarlenaInOutFuncs for PgVarlenaThing {
Self: Copy + Sized,
{
let s = input.to_str().unwrap();
let mut result: PgVarlena<PgVarlenaThing> = PgVarlena::new();

// parsing a string in this format: 1;2;3;[1,2,3,4,5]

let mut parts = s.split(';');
result.a = parts.next().unwrap().parse().unwrap();
result.b = parts.next().unwrap().parse().unwrap();
result.c = parts.next().unwrap().parse().unwrap();
let a = parts.next().unwrap().parse().unwrap();
let b = parts.next().unwrap().parse().unwrap();
let c = parts.next().unwrap().parse().unwrap();

let d = parts.next().unwrap();
let d = d.trim_start_matches('[');
let d = d.trim_end_matches(']');
let d = d.split(',').map(|e| e.parse().unwrap()).collect::<Vec<u8>>();
result.d = [d[0], d[1], d[2], d[3], d[4]];
result
PgVarlena::from(PgVarlenaThing::new(a, b, c, [d[0], d[1], d[2], d[3], d[4]]))
}

fn output(&self, buffer: &mut StringInfo) {
Expand Down
2 changes: 1 addition & 1 deletion pgrx-examples/postgres_type_variants/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plus `PostgresEnum` and the `PostgresEq`/`Ord`/`Hash` derive chain.
|---|---|---|---|
| `json_default.rs` | `#[derive(PostgresType, Serialize, Deserialize)]` | Serde JSON | The shape is JSON-friendly; you're prototyping; you don't need a custom textual form. |
| `inoutfuncs_custom.rs` | `#[derive(PostgresType)] #[inoutfuncs]` + `impl InOutFuncs` | Custom text via `&CStr` / `StringInfo` | The SQL surface should look like a domain syntax (`"3+4i"`, `"R5C7"`). |
| `varlena_zerocopy.rs` | `#[derive(PostgresType)] #[pgvarlena_inoutfuncs]` + `impl PgVarlenaInOutFuncs` | Fixed binary on disk; custom text at SQL boundary | High-volume reads; tight binary layout pays off; you can guarantee `#[repr(C)]`-stable fields. |
| `varlena_binary.rs` | `#[derive(PostgresType)] #[pgvarlena_inoutfuncs]` + `impl PgVarlenaInOutFuncs` | Fixed binary on disk, copied without parsing on read; custom text at SQL boundary | High-volume reads; tight binary layout pays off; you can guarantee `#[repr(C)]`-stable fields. |
| `handrolled_datum.rs` | None — manual `FromDatum`/`IntoDatum` + `extension_sql!` | Whatever you write | You need full control over the SQL representation, alignment, or storage class; or you're studying what the derive generates. |

## Related derives
Expand Down
2 changes: 1 addition & 1 deletion pgrx-examples/postgres_type_variants/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod composite_and_array;
mod handrolled_datum;
mod inoutfuncs_custom;
mod json_default;
mod varlena_zerocopy;
mod varlena_binary;

pgrx::pg_module_magic!(name, version);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
//LICENSE
//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.

//! # Variant 3: zero-copy varlena via `PgVarlenaInOutFuncs`
//! # Variant 3: fixed binary layout via `PgVarlenaInOutFuncs`
//!
//! `#[pgvarlena_inoutfuncs]` makes the type an on-disk fixed binary layout that
//! Postgres can store directly without serialization. Use this when:
//! `#[pgvarlena_inoutfuncs]` stores the bytes of a `Copy` struct in a varlena,
//! with no serialization format. Use this when:
//!
//! * The struct is `Copy` and `#[repr(C)]`-compatible.
//! * The struct is `Copy` and `#[repr(C)]`, and holds plain data: no
//! references or pointers.
//! * You frequently read large numbers of rows and Serde JSON would dominate.
//! * You're willing to define a textual form for I/O at SQL boundaries.
//!
//! The trade-off vs. variant 2 is performance (no parse on read) for cost
//! (you must hand-write text I/O AND keep the binary layout stable).
//! Reading a value copies its bytes into an aligned Rust value without
//! parsing them, and returning one allocates a new varlena. The trade-off vs.
//! variant 2 is performance (no parse on read) for cost (you must hand-write
//! text I/O AND keep the binary layout stable).
//!
//! Note: `Rgb` is 3 bytes — a tight layout is the whole point of this variant.

Expand All @@ -30,6 +33,7 @@ use pgrx::{PgVarlenaInOutFuncs, StringInfo};
#[derive(Copy, Clone, PostgresType)]
#[bikeshed_postgres_type_manually_impl_from_into_datum]
#[pgvarlena_inoutfuncs]
#[repr(C)]
pub struct Rgb {
pub r: u8,
pub g: u8,
Expand All @@ -45,11 +49,7 @@ impl PgVarlenaInOutFuncs for Rgb {
let r = u8::from_str_radix(&hex[0..2], 16).expect("bad red");
let g = u8::from_str_radix(&hex[2..4], 16).expect("bad green");
let b = u8::from_str_radix(&hex[4..6], 16).expect("bad blue");
let mut v = PgVarlena::<Self>::new();
v.r = r;
v.g = g;
v.b = b;
v
PgVarlena::from(Rgb { r, g, b })
}

fn output(&self, buffer: &mut StringInfo) {
Expand All @@ -59,7 +59,8 @@ impl PgVarlenaInOutFuncs for Rgb {

#[pg_extern]
fn rgb_luminance(c: PgVarlena<Rgb>) -> f64 {
// Rec. 601 luma — purely a demonstration; arithmetic happens directly on the varlena's fields with no copy or parse.
// Rec. 601 luma — purely a demonstration. The fields come from a copy of the
// stored bytes, with no parse.
0.299 * c.r as f64 + 0.587 * c.g as f64 + 0.114 * c.b as f64
}

Expand Down
5 changes: 2 additions & 3 deletions pgrx-examples/schemas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ fn return_vec_of_customtype() -> Vec<SomeStruct> {
}
```

In general this is only necessary when returning a `Vec<T: PostgresType>`. In this situation, pgrx needs to know that type's
`oid` (from the `pg_catalog.pg_type` system catalog) and as such, the schema in which that type lives must be on that
function's `search_path`.
pgrx itself does not need it to find a `#[derive(PostgresType)]` type's `oid`, for example when returning a
`Vec<T: PostgresType>`: it looks for the type among the types that belong to the extension, in whatever schema they are.

### Relocatable extensions

Expand Down
45 changes: 37 additions & 8 deletions pgrx-examples/subtrans_infos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ type TransactionId = pg_sys::TransactionId;

/// RAII wrapper for PostgreSQL LWLock to ensure proper release
///
/// This guard acquires a shared lock and releases it when dropped, including while pgrx
/// unwinds a PostgreSQL ERROR. `errfinish` has reset `InterruptHoldoffCount` to zero by then,
/// so that release fails `LWLockRelease`'s `RESUME_INTERRUPTS` assertion on assert-enabled
/// servers.
/// This guard acquires a shared lock and releases it when dropped, including while a Rust
/// panic unwinds. A PostgreSQL ERROR is different: `errfinish` resets
/// `InterruptHoldoffCount` to zero before pgrx turns the error into an unwinding panic, and
/// `LWLockRelease` would then fail its `RESUME_INTERRUPTS` assertion. The guard leaves the
/// lock to transaction abort's `LWLockReleaseAll` in that case, as pgrx's own lock guards do.
struct PgLwLockGuard {
lock: *mut pg_sys::LWLock,
}
Expand Down Expand Up @@ -40,11 +41,13 @@ impl PgLwLockGuard {
impl Drop for PgLwLockGuard {
fn drop(&mut self) {
// SAFETY: new_shared acquired this live lock for this guard on the backend thread,
// and this releases that acquisition once. While a PostgreSQL ERROR unwinds, the
// release fails an assertion on assert-enabled servers, as the type's documentation
// describes.
// which is the only thread that reads InterruptHoldoffCount. LWLockAcquire holds
// interrupts, so the count stays positive while the lock is held unless a
// PostgreSQL ERROR reset it; transaction abort then releases the lock instead.
unsafe {
pg_sys::LWLockRelease(self.lock);
if pg_sys::InterruptHoldoffCount > 0 {
pg_sys::LWLockRelease(self.lock);
}
}
}
}
Expand Down Expand Up @@ -397,6 +400,32 @@ mod tests {
}
}

/// A PostgreSQL ERROR while the guard holds the lock resets InterruptHoldoffCount to 0.
/// LWLockRelease in Drop would then decrement it past 0, which assert-enabled servers stop
/// in RESUME_INTERRUPTS and other servers let wrap. The guard must leave the lock held for
/// transaction abort, so the test checks both before it rethrows the ERROR.
#[pg_test(error = "cache lookup failed for type 0")]
fn truncation_lock_guard_survives_a_postgres_error() {
// SAFETY: pg_test runs in an initialized backend, and the generated pointer names the
// permanent XactTruncationLock.
let lock = unsafe { pg_sys::XactTruncationLock!().get() };
PgTryBuilder::new(|| {
// SAFETY: as above. format_type_extended raises an ERROR for type 0, which unwinds
// through the guard.
unsafe {
let _guard = super::PgLwLockGuard::new_shared(lock);
pg_sys::format_type_extended(pg_sys::Oid::INVALID, -1, 0);
}
})
.catch_others(|error| {
// SAFETY: both reads run on the backend thread, and the lock is permanent.
let state = unsafe { (pg_sys::InterruptHoldoffCount, pg_sys::LWLockHeldByMe(lock)) };
assert_eq!(state, (0, true), "(InterruptHoldoffCount, LWLockHeldByMe) after the ERROR");
error.rethrow()
})
.execute();
}

/// Test basic functionality with current transaction ID
#[pg_test]
fn test_basic_function() {
Expand Down
Loading
Loading