From 72b36d78f9d279975bc6973aea7c13fb1852603d Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 17 Jul 2026 23:30:30 +0800 Subject: [PATCH 1/2] feat: treat null array as text array --- arrow-pg/src/datatypes.rs | 17 ++++++++++ arrow-pg/src/encoder.rs | 61 ++++++++++++++++++++++++++++++++++++ arrow-pg/src/list_encoder.rs | 7 ++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/arrow-pg/src/datatypes.rs b/arrow-pg/src/datatypes.rs index 48dfaf5..142a5d0 100644 --- a/arrow-pg/src/datatypes.rs +++ b/arrow-pg/src/datatypes.rs @@ -52,6 +52,10 @@ pub fn into_pg_type(arrow_type: &DataType) -> PgWireResult { | DataType::LargeList(field) | DataType::ListView(field) | DataType::LargeListView(field) => match field.data_type() { + // Align with PostgreSQL: an array literal without type + // information such as `ARRAY[NULL]` has a `Null` element type and + // is reported as `text[]` by postgres. + DataType::Null => Type::TEXT_ARRAY, DataType::Boolean => Type::BOOL_ARRAY, DataType::Int8 => Type::INT2_ARRAY, DataType::Int16 | DataType::UInt8 => Type::INT2_ARRAY, @@ -186,3 +190,16 @@ pub fn encode_recordbatch( let mut row_stream = RowEncoder::new(record_batch, fields); Box::new(std::iter::from_fn(move || row_stream.next_row())) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn null_list_is_text_array() { + // Align with PostgreSQL: `ARRAY[NULL]` has no element type + // information and is reported as `text[]`. + let ty = DataType::List(Arc::new(Field::new_list_field(DataType::Null, true))); + assert_eq!(into_pg_type(&ty).unwrap(), Type::TEXT_ARRAY); + } +} diff --git a/arrow-pg/src/encoder.rs b/arrow-pg/src/encoder.rs index bd9b3a6..4a29f40 100644 --- a/arrow-pg/src/encoder.rs +++ b/arrow-pg/src/encoder.rs @@ -642,6 +642,67 @@ mod tests { ); } + #[test] + fn encodes_null_list_as_text_array() { + // Regression: `ARRAY[NULL]` (a List whose element type is Null) must + // be encoded as a `text[]` value `{NULL}`, aligning with postgres + // rather than emitting a SQL NULL. + #[derive(Default)] + struct TextEncoder { + encoded_value: String, + } + + impl Encoder for TextEncoder { + type Item = String; + + fn encode_field(&mut self, value: &T, pg_field: &FieldInfo) -> PgWireResult<()> + where + T: ToSql + ToSqlText + Sized, + { + let mut bytes = BytesMut::new(); + value + .to_sql_text( + pg_field.datatype(), + &mut bytes, + &FormatOptions::default(), + ) + .unwrap(); + self.encoded_value = String::from_utf8(bytes.to_vec()).unwrap(); + Ok(()) + } + + fn take_row(&mut self) -> Self::Item { + std::mem::take(&mut self.encoded_value) + } + } + + // Build a single-row ListArray whose element type is Null, mirroring + // DataFusion's `array[null]` output. + let list_field = Arc::new(Field::new_list_field(DataType::Null, true)); + let offsets = arrow::buffer::OffsetBuffer::::from_lengths([1]); + let values = Arc::new(NullArray::new(1)) as Arc; + let list_arr: Arc = Arc::new(ListArray::new( + list_field.clone(), + offsets, + values, + None, + )); + + let arrow_field = Field::new("c", DataType::List(list_field), true); + let pg_field = FieldInfo::new( + "c".to_string(), + None, + None, + Type::TEXT_ARRAY, + FieldFormat::Text, + ); + + let mut encoder = TextEncoder::default(); + let result = encode_value(&mut encoder, &list_arr, 0, &arrow_field, &pg_field); + assert!(result.is_ok()); + assert_eq!(encoder.encoded_value, "{NULL}"); + } + #[test] fn test_get_time32_second_value() { let array = Time32SecondArray::from_iter_values([3723_i32]); diff --git a/arrow-pg/src/list_encoder.rs b/arrow-pg/src/list_encoder.rs index 2db41f8..550584c 100644 --- a/arrow-pg/src/list_encoder.rs +++ b/arrow-pg/src/list_encoder.rs @@ -106,7 +106,12 @@ pub fn encode_list( ) -> PgWireResult<()> { match arr.data_type() { DataType::Null => { - encoder.encode_field(&None::, pg_field)?; + // The element type is unknown (e.g. `ARRAY[NULL]`). Align with + // PostgreSQL and treat the list as `text[]`, preserving the + // number of (null) elements so that `ARRAY[NULL]` yields `{NULL}` + // rather than a SQL NULL. + let value: Vec> = (0..arr.len()).map(|_| None).collect(); + encoder.encode_field(&value, pg_field)?; Ok(()) } DataType::Boolean => { From 076e03cea18578c7081f8ca14404df511af48f08 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 17 Jul 2026 23:32:06 +0800 Subject: [PATCH 2/2] chore: fmt --- arrow-pg/src/encoder.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/arrow-pg/src/encoder.rs b/arrow-pg/src/encoder.rs index 4a29f40..91f35f6 100644 --- a/arrow-pg/src/encoder.rs +++ b/arrow-pg/src/encoder.rs @@ -661,11 +661,7 @@ mod tests { { let mut bytes = BytesMut::new(); value - .to_sql_text( - pg_field.datatype(), - &mut bytes, - &FormatOptions::default(), - ) + .to_sql_text(pg_field.datatype(), &mut bytes, &FormatOptions::default()) .unwrap(); self.encoded_value = String::from_utf8(bytes.to_vec()).unwrap(); Ok(()) @@ -681,12 +677,8 @@ mod tests { let list_field = Arc::new(Field::new_list_field(DataType::Null, true)); let offsets = arrow::buffer::OffsetBuffer::::from_lengths([1]); let values = Arc::new(NullArray::new(1)) as Arc; - let list_arr: Arc = Arc::new(ListArray::new( - list_field.clone(), - offsets, - values, - None, - )); + let list_arr: Arc = + Arc::new(ListArray::new(list_field.clone(), offsets, values, None)); let arrow_field = Field::new("c", DataType::List(list_field), true); let pg_field = FieldInfo::new(