Skip to content

Commit e60a8e4

Browse files
committed
fix: detoast borrowed FlatArray arguments
1 parent 58147f1 commit e60a8e4

5 files changed

Lines changed: 167 additions & 38 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ impl PgExtern {
469469
let result = match call_flow {
470470
::pgrx::callconv::CallCx::WrappedFn(mcx) => {
471471
let mut mcx = ::pgrx::PgMemoryContexts::For(mcx);
472-
let #args_ident = &mut fcinfo.args();
472+
let mut #args_ident = fcinfo.args();
473473
let call_result = mcx.switch_to(|_| {
474474
#(#arg_fetches)*
475475
#func_name( #(#arg_pats),* )

pgrx-unit-tests/src/tests/array_borrowed.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ fn borrow_get_arr_nelems(arr: &FlatArray<'_, i32>) -> libc::c_int {
9595
arr.nelems() as _
9696
}
9797

98+
#[pg_extern]
99+
fn borrow_iter_array<'a>(arr: &'a FlatArray<'a, i32>) -> SetOfIterator<'a, i32> {
100+
SetOfIterator::new(arr.iter_non_null().copied())
101+
}
102+
98103
#[pg_extern]
99104
fn borrow_get_arr_data_ptr_nth_elem(arr: &FlatArray<'_, i32>, elem: i32) -> Option<i32> {
100105
arr.get(elem as usize).unwrap().into_option().copied()
@@ -181,7 +186,7 @@ mod tests {
181186
use pgrx::datum::DatumWithOid;
182187
use pgrx::memcx;
183188
use pgrx::prelude::*;
184-
use pgrx::{IntoDatum, Json};
189+
use pgrx::{IntoDatum, Json, PgMemoryContexts, direct_pg_extern_function_call};
185190
use serde_json::json;
186191

187192
#[pg_test]
@@ -330,6 +335,67 @@ mod tests {
330335
assert_eq!(len, Ok(Some(5)));
331336
}
332337

338+
#[pg_test]
339+
fn borrow_test_toasted_flat_array() -> Result<(), pgrx::spi::Error> {
340+
Spi::run("CREATE TEMP TABLE flat_array_toast_test (arr integer[])")?;
341+
Spi::run("ALTER TABLE flat_array_toast_test ALTER COLUMN arr SET STORAGE EXTERNAL")?;
342+
Spi::run(
343+
"INSERT INTO flat_array_toast_test \
344+
SELECT array_agg(i) FROM generate_series(1, 2500) i",
345+
)?;
346+
347+
let result = Spi::get_two::<i32, i32>(
348+
"SELECT borrow_get_arr_nelems(arr), borrow_sum_array(arr) \
349+
FROM flat_array_toast_test",
350+
);
351+
assert_eq!(result, Ok((Some(2500), Some(3_126_250))));
352+
353+
let iterated = Spi::get_two::<i64, i64>(
354+
"SELECT count(*), sum(value) \
355+
FROM flat_array_toast_test, LATERAL borrow_iter_array(arr) value",
356+
);
357+
assert_eq!(iterated, Ok((Some(2500), Some(3_126_250))));
358+
359+
Spi::connect(|client| {
360+
let table =
361+
client.select("SELECT arr FROM flat_array_toast_test", Some(1), &[])?.first();
362+
let datum = table.get_datum_by_ordinal(1)?.expect("array was null");
363+
assert!(unsafe { pgrx::varlena::varatt_is_1b_e(datum.cast_mut_ptr()) });
364+
365+
unsafe {
366+
PgMemoryContexts::Transient {
367+
parent: PgMemoryContexts::CurrentMemoryContext.value(),
368+
name: "toasted FlatArray cleanup test",
369+
min_context_size: 8 * 1024,
370+
initial_block_size: 8 * 1024,
371+
max_block_size: 8 * 1024,
372+
}
373+
.switch_to(|context| {
374+
let call = || {
375+
direct_pg_extern_function_call::<i32>(
376+
super::borrow_get_arr_nelems_wrapper,
377+
&[Some(datum)],
378+
)
379+
};
380+
381+
assert_eq!(call(), Some(2500));
382+
let warmed = pg_sys::MemoryContextMemAllocated(context.value(), true);
383+
for _ in 0..64 {
384+
assert_eq!(call(), Some(2500));
385+
}
386+
let after = pg_sys::MemoryContextMemAllocated(context.value(), true);
387+
388+
assert!(
389+
after <= warmed + 64 * 1024,
390+
"detoasted arguments accumulated {} bytes",
391+
after - warmed
392+
);
393+
});
394+
}
395+
Ok(())
396+
})
397+
}
398+
333399
#[pg_test]
334400
fn borrow_test_get_arr_data_ptr_nth_elem() {
335401
let nth =

pgrx/src/array/flat_array.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ fn alloc_zeroed_head(
316316

317317
unsafe impl<T: ?Sized> BorrowDatum for FlatArray<'_, T> {
318318
const PASS: layout::PassBy = layout::PassBy::Ref;
319+
319320
unsafe fn point_from(ptr: ptr::NonNull<u8>) -> ptr::NonNull<Self> {
320321
unsafe {
321322
let len =
@@ -325,6 +326,22 @@ unsafe impl<T: ?Sized> BorrowDatum for FlatArray<'_, T> {
325326
)
326327
}
327328
}
329+
330+
unsafe fn borrow_arg_unchecked<'dat>(
331+
ptr: ptr::NonNull<u8>,
332+
register: impl FnOnce(ptr::NonNull<u8>),
333+
) -> &'dat Self {
334+
// SAFETY: The caller guarantees `ptr` points to a valid PostgreSQL array Datum.
335+
let detoasted = unsafe { pg_sys::pg_detoast_datum(ptr.as_ptr().cast()) };
336+
let detoasted = ptr::NonNull::new(detoasted).expect("pg_detoast_datum returned null");
337+
if detoasted.cast() != ptr {
338+
register(detoasted.cast());
339+
}
340+
341+
// SAFETY: `pg_detoast_datum` returns an aligned, contiguous, initialized ArrayType, and a
342+
// fresh allocation is registered above to live until the function result has been boxed.
343+
unsafe { Self::point_from(detoasted.cast()).as_ref() }
344+
}
328345
}
329346

330347
/// `T[]` in Postgres

pgrx/src/callconv.rs

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,10 @@ use crate::rel::PgRelation;
2727
use crate::{PgBox, PgMemoryContexts};
2828

2929
use core::marker::PhantomData;
30-
use core::{iter, mem, ptr, slice};
30+
use core::{mem, ptr};
3131
use std::ffi::{CStr, CString};
3232
use std::ptr::NonNull;
3333

34-
struct ReadOnly<T>(T);
35-
36-
impl<T> ReadOnly<T> {
37-
unsafe fn refer_to(&self) -> &T {
38-
&self.0
39-
}
40-
}
41-
42-
unsafe impl<T> Sync for ReadOnly<T> {}
43-
44-
static VIRTUAL_ARGUMENT: ReadOnly<pg_sys::NullableDatum> =
45-
ReadOnly(pg_sys::NullableDatum { value: pg_sys::Datum::null(), isnull: true });
46-
4734
/// How to pass a value from Postgres to Rust
4835
///
4936
/// This bound is necessary to distinguish things which can be passed into a `#[pg_extern] fn`.
@@ -288,7 +275,7 @@ where
288275
};
289276
unsafe {
290277
let ptr = ptr::NonNull::new_unchecked(ptr);
291-
T::borrow_unchecked(ptr)
278+
T::borrow_arg_unchecked(ptr, |ptr| arg.0.register_detoasted_arg(ptr))
292279
}
293280
}
294281

@@ -299,7 +286,11 @@ where
299286
});
300287
match (arg.is_null(), ptr) {
301288
(true, _) | (false, None) => Nullable::Null,
302-
(false, Some(ptr)) => unsafe { Nullable::Valid(T::borrow_unchecked(ptr)) },
289+
(false, Some(ptr)) => unsafe {
290+
Nullable::Valid(T::borrow_arg_unchecked(ptr, |ptr| {
291+
arg.0.register_detoasted_arg(ptr)
292+
}))
293+
},
303294
}
304295
}
305296
}
@@ -599,8 +590,26 @@ where
599590

600591
type FcInfoData = pg_sys::FunctionCallInfoBaseData;
601592

602-
#[derive(Clone)]
603-
pub struct FcInfo<'fcx>(pgrx_pg_sys::FunctionCallInfo, PhantomData<&'fcx mut FcInfoData>);
593+
/// A uniquely owned PostgreSQL function call frame.
594+
///
595+
/// Argument decoding may register temporary detoast allocations here, so this type is intentionally
596+
/// not cloneable.
597+
pub struct FcInfo<'fcx>(
598+
pgrx_pg_sys::FunctionCallInfo,
599+
PhantomData<&'fcx mut FcInfoData>,
600+
Vec<NonNull<u8>>,
601+
pg_sys::MemoryContext,
602+
);
603+
604+
impl Drop for FcInfo<'_> {
605+
fn drop(&mut self) {
606+
for allocation in self.2.drain(..) {
607+
// SAFETY: `BorrowDatum::borrow_arg_unchecked` requires registered pointers to be
608+
// PostgreSQL allocations that can be released after the function result is boxed.
609+
unsafe { pg_sys::pfree(allocation.as_ptr().cast()) }
610+
}
611+
}
612+
}
604613

605614
// when talking about this, there's the lifetime for setreturningfunction, and then there's the current context's lifetime.
606615
// Potentially <'srf, 'curr, 'ret: 'curr + 'srf> -> <'ret> but don't start with that.
@@ -620,7 +629,19 @@ impl<'fcx> FcInfo<'fcx> {
620629
#[inline]
621630
pub unsafe fn from_ptr(fcinfo: pg_sys::FunctionCallInfo) -> FcInfo<'fcx> {
622631
let _nullptr_check = NonNull::new(fcinfo).expect("fcinfo pointer must be non-null");
623-
Self(fcinfo, PhantomData)
632+
Self(fcinfo, PhantomData, Vec::new(), unsafe { pg_sys::CurrentMemoryContext })
633+
}
634+
635+
fn register_detoasted_arg(&mut self, ptr: NonNull<u8>) {
636+
// Set-returning functions unbox their arguments in a multi-call context so an iterator
637+
// may retain borrows across calls. That context owns its detoast allocations and releases
638+
// them when the SRF finishes. Ordinary functions unbox in the entry context, where we can
639+
// release fresh detoast copies as soon as their result has been boxed.
640+
if unsafe { pg_sys::CurrentMemoryContext } != self.3 {
641+
return;
642+
}
643+
644+
self.2.push(ptr);
624645
}
625646
/// Retrieve the arguments to this function call as a slice of [`pgrx_pg_sys::NullableDatum`]
626647
#[inline]
@@ -830,14 +851,18 @@ impl<'fcx> FcInfo<'fcx> {
830851
}
831852
}
832853

854+
/// Create a decoder for this function call's arguments.
855+
///
856+
/// The decoder borrows the call frame exclusively so temporary argument allocations have one
857+
/// unambiguous owner.
833858
#[inline]
834-
pub fn args<'arg>(&'arg self) -> Args<'arg, 'fcx> {
835-
Args { iter: self.raw_args().iter().enumerate(), fcinfo: self }
859+
pub fn args<'arg>(&'arg mut self) -> Args<'arg, 'fcx> {
860+
Args { next: 0, fcinfo: self }
836861
}
837862
}
838863

839864
// TODO: rebadge this as AnyElement
840-
pub struct Arg<'a, 'fcx>(&'a FcInfo<'fcx>, usize, &'a pg_sys::NullableDatum);
865+
pub struct Arg<'a, 'fcx>(&'a mut FcInfo<'fcx>, usize, pg_sys::NullableDatum);
841866

842867
impl<'a, 'fcx> Arg<'a, 'fcx> {
843868
/// # Performance note
@@ -880,23 +905,30 @@ impl<'a, 'fcx> Arg<'a, 'fcx> {
880905
}
881906
}
882907

908+
/// A lending-style decoder for PostgreSQL function arguments.
909+
///
910+
/// This is deliberately not an [`Iterator`]: each decoded [`Arg`] temporarily borrows the call
911+
/// frame's detoast cleanup state.
883912
pub struct Args<'a, 'fcx> {
884-
iter: iter::Enumerate<slice::Iter<'a, pg_sys::NullableDatum>>,
885-
fcinfo: &'a FcInfo<'fcx>,
886-
}
887-
888-
impl<'a, 'fcx> Iterator for Args<'a, 'fcx> {
889-
type Item = Arg<'a, 'fcx>;
890-
891-
fn next(&mut self) -> Option<Self::Item> {
892-
self.iter.next().map(|(a, b)| Arg(self.fcinfo, a, b))
893-
}
913+
next: usize,
914+
fcinfo: &'a mut FcInfo<'fcx>,
894915
}
895916

896917
impl<'a, 'fcx> Args<'a, 'fcx> {
897918
// generate an argument for use by virtual args
898-
fn synthesize_virtual_arg(&self) -> Arg<'a, 'fcx> {
899-
Arg(self.fcinfo, usize::MAX, unsafe { VIRTUAL_ARGUMENT.refer_to() })
919+
fn synthesize_virtual_arg(&mut self) -> Arg<'_, 'fcx> {
920+
Arg(
921+
self.fcinfo,
922+
usize::MAX,
923+
pg_sys::NullableDatum { value: pg_sys::Datum::null(), isnull: true },
924+
)
925+
}
926+
927+
fn next_raw(&mut self) -> Option<Arg<'_, 'fcx>> {
928+
let index = self.next;
929+
let datum = self.fcinfo.raw_args().get(index).copied()?;
930+
self.next += 1;
931+
Some(Arg(self.fcinfo, index, datum))
900932
}
901933

902934
/// # Safety
@@ -910,7 +942,7 @@ impl<'a, 'fcx> Args<'a, 'fcx> {
910942
unsafe { Some(T::unbox_arg_unchecked(self.synthesize_virtual_arg())) }
911943
} else {
912944
// SAFETY: caller upholds
913-
unsafe { self.next().map(|next| T::unbox_arg_unchecked(next)) }
945+
unsafe { self.next_raw().map(|next| T::unbox_arg_unchecked(next)) }
914946
}
915947
}
916948

@@ -923,7 +955,7 @@ impl<'a, 'fcx> Args<'a, 'fcx> {
923955
unsafe { Some(T::unbox_nullable_arg(self.synthesize_virtual_arg())) }
924956
} else {
925957
// SAFETY: caller upholds
926-
unsafe { self.next().map(|next| T::unbox_nullable_arg(next)) }
958+
unsafe { self.next_raw().map(|next| T::unbox_nullable_arg(next)) }
927959
}
928960
}
929961
}

pgrx/src/datum/borrow.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ pub unsafe trait BorrowDatum {
6363
unsafe fn borrow_unchecked<'dat>(ptr: ptr::NonNull<u8>) -> &'dat Self {
6464
unsafe { BorrowDatum::point_from(ptr).as_ref() }
6565
}
66+
67+
/// Prepare and borrow a function argument, registering any temporary allocation for cleanup.
68+
///
69+
/// This is used by pgrx's function-call machinery. Implementors that must copy an argument
70+
/// before it can be borrowed should pass that allocation to `register` before returning the
71+
/// borrow. The allocation must have been made by PostgreSQL and ownership is transferred to
72+
/// `register`, which keeps it alive for the function's return ABI and releases it afterward.
73+
#[doc(hidden)]
74+
unsafe fn borrow_arg_unchecked<'dat>(
75+
ptr: ptr::NonNull<u8>,
76+
_register: impl FnOnce(ptr::NonNull<u8>),
77+
) -> &'dat Self {
78+
unsafe { BorrowDatum::borrow_unchecked(ptr) }
79+
}
6680
}
6781

6882
/// From a pointer to a Datum, obtain a pointer to T's bytes

0 commit comments

Comments
 (0)