Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions arrow-pg/src/datatypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ pub fn into_pg_type(arrow_type: &DataType) -> PgWireResult<Type> {
| 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,
Expand Down Expand Up @@ -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);
}
}
53 changes: 53 additions & 0 deletions arrow-pg/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,59 @@ 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<T>(&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::<i32>::from_lengths([1]);
let values = Arc::new(NullArray::new(1)) as Arc<dyn Array>;
let list_arr: Arc<dyn Array> =
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]);
Expand Down
7 changes: 6 additions & 1 deletion arrow-pg/src/list_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ pub fn encode_list<T: Encoder>(
) -> PgWireResult<()> {
match arr.data_type() {
DataType::Null => {
encoder.encode_field(&None::<i8>, 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<Option<&str>> = (0..arr.len()).map(|_| None).collect();
encoder.encode_field(&value, pg_field)?;
Ok(())
}
DataType::Boolean => {
Expand Down
Loading