Skip to content

Commit 4ef7a51

Browse files
committed
Make &FlatArray<Text> serializable to JSON
* Text varlena element for FlatArray * Serialize FlatArray<Text> * cover mixed-stride + empty edge cases for FlatArray<Text>
1 parent 7827d0d commit 4ef7a51

4 files changed

Lines changed: 85 additions & 16 deletions

File tree

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

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use core::ffi::CStr;
1212
use pgrx::Json;
1313
use pgrx::PostgresEnum;
14-
use pgrx::array::{FlatArray, RawArray};
14+
use pgrx::array::{FlatArray, RawArray, Text};
1515
use pgrx::memcx::MemCx;
1616
use pgrx::nullable::Nullable;
1717
use pgrx::palloc::PBox;
@@ -68,11 +68,10 @@ fn borrow_optional_array_with_default(
6868
values.unwrap().iter().map(|v| v.into_option().copied().unwrap_or(0)).sum()
6969
}
7070

71-
// FIXME: replace with Text
72-
// #[pg_extern]
73-
// fn borrow_serde_serialize_array<'dat>(values: &FlatArray<'dat, &'dat str>) -> Json {
74-
// Json(json! { { "values": values } })
75-
// }
71+
#[pg_extern]
72+
fn borrow_serde_serialize_array(values: &FlatArray<'_, pgrx::array::Text>) -> Json {
73+
Json(json! { { "values": values } })
74+
}
7675

7776
#[pg_extern]
7877
fn borrow_serde_serialize_array_i32(values: &FlatArray<'_, i32>) -> Json {
@@ -265,16 +264,36 @@ mod tests {
265264
assert_eq!(sum, Ok(Some(6f32)));
266265
}
267266

268-
// TODO: fix this test by redesigning SPI.
269-
// #[pg_test]
270-
// fn borrow_test_serde_serialize_array() -> Result<(), pgrx::spi::Error> {
271-
// let json = Spi::get_one::<Json>(
272-
// "SELECT borrow_serde_serialize_array(ARRAY['one', null, 'two', 'three'])",
273-
// )?
274-
// .expect("returned json was null");
275-
// assert_eq!(json.0, json! {{"values": ["one", null, "two", "three"]}});
276-
// Ok(())
277-
// }
267+
#[pg_test]
268+
fn borrow_test_serde_serialize_array() -> Result<(), pgrx::spi::Error> {
269+
let json = Spi::get_one::<Json>(
270+
"SELECT borrow_serde_serialize_array(ARRAY['one', null, 'two', 'three'])",
271+
)?
272+
.expect("returned json was null");
273+
assert_eq!(json.0, json! {{"values": ["one", null, "two", "three"]}});
274+
Ok(())
275+
}
276+
277+
#[pg_test]
278+
fn borrow_test_serde_serialize_array_mixed() -> Result<(), pgrx::spi::Error> {
279+
// Stride stress: empty string, null, multibyte, and a >127-byte element
280+
// (forces a 4-byte varlena header next to 1-byte ones). Mis-stride corrupts tail.
281+
let long = "x".repeat(200);
282+
let json = Spi::get_one::<Json>(&format!(
283+
"SELECT borrow_serde_serialize_array(ARRAY['', null, '日本語', '{long}', 'z'])"
284+
))?
285+
.expect("returned json was null");
286+
assert_eq!(json.0, json! {{"values": ["", null, "日本語", long, "z"]}});
287+
Ok(())
288+
}
289+
290+
#[pg_test]
291+
fn borrow_test_serde_serialize_array_empty() -> Result<(), pgrx::spi::Error> {
292+
let json = Spi::get_one::<Json>("SELECT borrow_serde_serialize_array(ARRAY[]::text[])")?
293+
.expect("returned json was null");
294+
assert_eq!(json.0, json! {{"values": []}});
295+
Ok(())
296+
}
278297

279298
#[pg_test]
280299
fn borrow_test_optional_array_with_default() {

pgrx/src/array.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod element;
3131
mod flat_array;
3232
mod port;
3333

34+
pub use crate::datum::Text;
3435
pub use element::Element;
3536
pub use flat_array::{ArrayAllocError, FlatArray};
3637

pgrx/src/array/flat_array.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,12 @@ where
395395
}
396396
}
397397

398+
impl serde::Serialize for FlatArray<'_, super::Text> {
399+
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
400+
s.collect_seq(self.iter().map(|n| n.into_option().map(|t| t.as_str())))
401+
}
402+
}
403+
398404
impl<'arr, T> ExactSizeIterator for ArrayIter<'arr, T> where T: ?Sized + Element {}
399405
impl<'arr, T> FusedIterator for ArrayIter<'arr, T> where T: ?Sized + Element {}
400406

pgrx/src/datum/borrow.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,46 @@ unsafe impl Element for ffi::CStr {
140140
unsafe { ffi::CStr::from_ptr(char_ptr) }
141141
}
142142
}
143+
144+
/// A borrowed Postgres `text` varlena, usable as a `FlatArray` element.
145+
#[repr(transparent)]
146+
pub struct Text([u8]);
147+
148+
impl Text {
149+
/// The string contents (UTF-8), excluding the varlena header.
150+
pub fn as_str(&self) -> &str {
151+
// SAFETY: tail is a valid varlena; Postgres text is UTF-8 in pgrx's server encodings
152+
unsafe { crate::varlena::text_to_rust_str_unchecked(self.0.as_ptr().cast()) }
153+
}
154+
}
155+
156+
unsafe impl DatumPass for Text {
157+
const PASS: PassBy = PassBy::Ref;
158+
}
159+
unsafe impl Element for Text {
160+
unsafe fn point_from(ptr: ptr::NonNull<u8>) -> ptr::NonNull<Self> {
161+
// full varlena size so size_of_val == storage, keeping ArrayIter stride sound
162+
let len = unsafe { crate::varlena::varsize_any(ptr.as_ptr().cast()) };
163+
unsafe {
164+
ptr::NonNull::new_unchecked(
165+
ptr::slice_from_raw_parts_mut(ptr.as_ptr(), len) as *mut Self
166+
)
167+
}
168+
}
169+
}
170+
171+
unsafe impl pgrx_sql_entity_graph::metadata::SqlTranslatable for Text {
172+
const TYPE_IDENT: &'static str = "Text";
173+
const TYPE_ORIGIN: pgrx_sql_entity_graph::metadata::TypeOrigin =
174+
pgrx_sql_entity_graph::metadata::TypeOrigin::External;
175+
const ARGUMENT_SQL: Result<
176+
pgrx_sql_entity_graph::metadata::SqlMappingRef,
177+
pgrx_sql_entity_graph::metadata::ArgumentError,
178+
> = Ok(pgrx_sql_entity_graph::metadata::SqlMappingRef::literal("text"));
179+
const RETURN_SQL: Result<
180+
pgrx_sql_entity_graph::metadata::ReturnsRef,
181+
pgrx_sql_entity_graph::metadata::ReturnsError,
182+
> = Ok(pgrx_sql_entity_graph::metadata::ReturnsRef::One(
183+
pgrx_sql_entity_graph::metadata::SqlMappingRef::literal("text"),
184+
));
185+
}

0 commit comments

Comments
 (0)