Skip to content

Commit 882693a

Browse files
committed
Fix error message to include Text as a valid type for FlatArray
Add function to calculate total length of non-null Text elements in FlatArray and add more tests for serializing FlatArray<Text>
1 parent 4ef7a51 commit 882693a

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ fn borrow_serde_serialize_array_i32(values: &FlatArray<'_, i32>) -> Json {
7979
Json(json! { { "values": values } })
8080
}
8181

82+
// Exercises `Text::as_str` directly (not via serde): sum of UTF-8 byte lengths of the non-null elements. Verifies the varlena header is excluded from the data.
83+
#[pg_extern]
84+
fn borrow_text_total_len(values: &FlatArray<'_, pgrx::array::Text>) -> i64 {
85+
values.iter().filter_map(|n| n.into_option()).map(|t| t.as_str().len() as i64).sum()
86+
}
87+
8288
#[pg_extern]
8389
fn borrow_return_text_array() -> Vec<&'static str> {
8490
vec!["a", "b", "c", "d"]
@@ -295,6 +301,73 @@ mod tests {
295301
Ok(())
296302
}
297303

304+
#[pg_test]
305+
fn borrow_test_serde_serialize_array_no_nulls() -> Result<(), pgrx::spi::Error> {
306+
// No nulls => Postgres omits the null bitmap, exercising the None-bitmap iterator branch.
307+
let json = Spi::get_one::<Json>("SELECT borrow_serde_serialize_array(ARRAY['a','b','c'])")?
308+
.expect("returned json was null");
309+
assert_eq!(json.0, json! {{"values": ["a", "b", "c"]}});
310+
Ok(())
311+
}
312+
313+
#[pg_test]
314+
fn borrow_test_serde_serialize_array_all_nulls() -> Result<(), pgrx::spi::Error> {
315+
let json = Spi::get_one::<Json>(
316+
"SELECT borrow_serde_serialize_array(ARRAY[null,null,null]::text[])",
317+
)?
318+
.expect("returned json was null");
319+
assert_eq!(json.0, json! {{"values": [null, null, null]}});
320+
Ok(())
321+
}
322+
323+
#[pg_test]
324+
fn borrow_test_serde_serialize_array_json_special_chars() -> Result<(), pgrx::spi::Error> {
325+
// Quote, backslash, newline, and tab must be serde-escaped, not emitted raw.
326+
let json = Spi::get_one::<Json>(
327+
r#"SELECT borrow_serde_serialize_array(ARRAY[E'a"b\\c', E'line1\nline2', E'\t'])"#,
328+
)?
329+
.expect("returned json was null");
330+
assert_eq!(json.0, json! {{"values": ["a\"b\\c", "line1\nline2", "\t"]}});
331+
Ok(())
332+
}
333+
334+
#[pg_test]
335+
fn borrow_test_serde_serialize_array_header_boundary() -> Result<(), pgrx::spi::Error> {
336+
// Postgres switches from a 1-byte to a 4-byte varlena header at ~126 bytes.
337+
// Place elements on both sides of that boundary adjacent to each other so a
338+
// wrong stride would corrupt the following element.
339+
let s125 = "a".repeat(125);
340+
let s126 = "b".repeat(126);
341+
let s127 = "c".repeat(127);
342+
let json = Spi::get_one::<Json>(&format!(
343+
"SELECT borrow_serde_serialize_array(ARRAY['{s125}','{s126}','{s127}','z'])"
344+
))?
345+
.expect("returned json was null");
346+
assert_eq!(json.0, json! {{"values": [s125, s126, s127, "z"]}});
347+
Ok(())
348+
}
349+
350+
#[pg_test]
351+
fn borrow_test_text_as_str_len() {
352+
// 'ab' (2) + null (skipped) + '日本語' (9 UTF-8 bytes) = 11; header excluded.
353+
let len = Spi::get_one::<i64>(
354+
"SELECT borrow_text_total_len(ARRAY['ab', null, '日本語']::text[])",
355+
);
356+
assert_eq!(len, Ok(Some(11)));
357+
}
358+
359+
#[pg_test]
360+
fn borrow_test_text_as_str_len_empty_and_all_null() {
361+
assert_eq!(
362+
Spi::get_one::<i64>("SELECT borrow_text_total_len(ARRAY[]::text[])"),
363+
Ok(Some(0))
364+
);
365+
assert_eq!(
366+
Spi::get_one::<i64>("SELECT borrow_text_total_len(ARRAY[null,null]::text[])"),
367+
Ok(Some(0))
368+
);
369+
}
370+
298371
#[pg_test]
299372
fn borrow_test_optional_array_with_default() {
300373
let sum = Spi::get_one::<i32>("SELECT borrow_optional_array_with_default(ARRAY[1,2,3])");

pgrx-unit-tests/tests/compile-fail/no-arrays-of-arrays.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ error[E0277]: the trait bound `FlatArray<'_, i32>: pgrx::array::Element` is not
99
Date
1010
Oid
1111
Point
12+
Text
1213
Time
1314
TimeWithTimeZone
1415
Timestamp
15-
TimestampWithTimeZone
1616
and $N others
1717
= note: required for `FlatArray<'_, FlatArray<'_, i32>>` to implement `SqlTranslatable`
1818
= note: 1 redundant requirement hidden

pgrx/src/datum/borrow.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,12 @@ unsafe impl Element for ffi::CStr {
146146
pub struct Text([u8]);
147147

148148
impl Text {
149-
/// The string contents (UTF-8), excluding the varlena header.
149+
/// The string contents, excluding the varlena header.
150+
///
151+
/// Uses pgrx's encoding-aware conversion: when the database encoding isn't guaranteed UTF-8 (e.g. `SQL_ASCII`), the bytes are validated and this panics on non-UTF-8 data — same behavior as reading a `text` datum as `&str`.
150152
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+
// SAFETY: tail is a valid varlena
154+
unsafe { crate::datum::from::convert_varlena_to_str_memoized(self.0.as_ptr().cast()) }
153155
}
154156
}
155157

pgrx/src/datum/from.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,9 @@ impl<'a> FromDatum for &'a str {
386386

387387
// This is not marked inline on purpose, to allow it to be in a single code section
388388
// which is then branch-predicted on every time by the CPU.
389-
unsafe fn convert_varlena_to_str_memoized<'a>(varlena: *const pg_sys::varlena) -> &'a str {
389+
pub(crate) unsafe fn convert_varlena_to_str_memoized<'a>(
390+
varlena: *const pg_sys::varlena,
391+
) -> &'a str {
390392
match *crate::UTF8DATABASE {
391393
crate::Utf8Compat::Yes => varlena::text_to_rust_str_unchecked(varlena),
392394
crate::Utf8Compat::Maybe => varlena::text_to_rust_str(varlena)

0 commit comments

Comments
 (0)