blob: f41cb7f918d8e83c0cc37f537c26f0aef5ac3f8f [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::csv_funcs::csv_write_options::CsvWriteOptions;
use crate::{spark_cast, EvalMode, SparkCastOptions};
use arrow::array::{as_string_array, as_struct_array, Array, ArrayRef, StringArray, StringBuilder};
use arrow::array::{RecordBatch, StructArray};
use arrow::datatypes::{DataType, Schema};
use datafusion::common::Result;
use datafusion::logical_expr::ColumnarValue;
use datafusion::physical_expr::PhysicalExpr;
use std::any::Any;
use std::fmt::{Display, Formatter};
use std::hash::Hash;
use std::sync::Arc;
/// to_csv spark function
#[derive(Debug, Eq)]
pub struct ToCsv {
expr: Arc<dyn PhysicalExpr>,
timezone: String,
csv_write_options: CsvWriteOptions,
}
impl Hash for ToCsv {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.expr.hash(state);
self.timezone.hash(state);
self.csv_write_options.hash(state);
}
}
impl PartialEq for ToCsv {
fn eq(&self, other: &Self) -> bool {
self.expr.eq(&other.expr)
&& self.timezone.eq(&other.timezone)
&& self.csv_write_options.eq(&other.csv_write_options)
}
}
impl ToCsv {
pub fn new(
expr: Arc<dyn PhysicalExpr>,
timezone: &str,
csv_write_options: CsvWriteOptions,
) -> Self {
Self {
expr,
timezone: timezone.to_owned(),
csv_write_options,
}
}
}
impl Display for ToCsv {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"to_csv({}, timezone={}, csv_write_options={})",
self.expr, self.timezone, self.csv_write_options
)
}
}
impl PhysicalExpr for ToCsv {
fn as_any(&self) -> &dyn Any {
self
}
fn data_type(&self, _: &Schema) -> Result<DataType> {
Ok(DataType::Utf8)
}
fn nullable(&self, input_schema: &Schema) -> Result<bool> {
self.expr.nullable(input_schema)
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
let input_array = self.expr.evaluate(batch)?.into_array(batch.num_rows())?;
let mut cast_options = SparkCastOptions::new(EvalMode::Legacy, &self.timezone, false);
cast_options.null_string = self.csv_write_options.null_value.clone();
let struct_array = as_struct_array(&input_array);
let csv_array = to_csv_inner(struct_array, &cast_options, &self.csv_write_options)?;
Ok(ColumnarValue::Array(csv_array))
}
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
vec![&self.expr]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
Ok(Arc::new(Self::new(
Arc::clone(&children[0]),
&self.timezone,
self.csv_write_options.clone(),
)))
}
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}
pub fn to_csv_inner(
array: &StructArray,
cast_options: &SparkCastOptions,
write_options: &CsvWriteOptions,
) -> Result<ArrayRef> {
let string_arrays: Vec<ArrayRef> = as_struct_array(&array)
.columns()
.iter()
.map(|array| {
spark_cast(
ColumnarValue::Array(Arc::clone(array)),
&DataType::Utf8,
cast_options,
)?
.into_array(array.len())
})
.collect::<Result<Vec<_>>>()?;
let string_arrays: Vec<&StringArray> = string_arrays
.iter()
.map(|array| as_string_array(array))
.collect();
let is_string: Vec<bool> = array
.fields()
.iter()
.map(|f| matches!(f.data_type(), DataType::Utf8 | DataType::LargeUtf8))
.collect();
let mut builder = StringBuilder::with_capacity(array.len(), array.len() * 16);
let mut csv_string = String::with_capacity(array.len() * 16);
let quote_char = write_options.quote.chars().next().unwrap_or('"');
let escape_char = write_options.escape.chars().next().unwrap_or('\\');
for row_idx in 0..array.len() {
if array.is_null(row_idx) {
builder.append_null();
} else {
csv_string.clear();
for (col_idx, column) in string_arrays.iter().enumerate() {
if col_idx > 0 {
csv_string.push_str(&write_options.delimiter);
}
if column.is_null(row_idx) {
if write_options.quote_all {
csv_string.push(quote_char);
}
csv_string.push_str(&write_options.null_value);
if write_options.quote_all {
csv_string.push(quote_char);
}
} else {
let mut value = column.value(row_idx);
let is_string_field = is_string[col_idx];
if is_string_field {
if write_options.ignore_leading_white_space {
value = value.trim_start();
}
if write_options.ignore_trailing_white_space {
value = value.trim_end();
}
}
let needs_quoting = write_options.quote_all
|| (is_string_field
&& (value.contains(&write_options.delimiter)
|| value.contains(quote_char)
|| value.contains('\n')
|| value.contains('\r'))
|| value.is_empty());
let needs_escaping = needs_quoting
&& (value.contains(quote_char) || value.contains(escape_char));
if needs_quoting {
csv_string.push(quote_char);
}
if needs_escaping {
escape_value(value, quote_char, escape_char, &mut csv_string);
} else {
csv_string.push_str(value);
}
if needs_quoting {
csv_string.push(quote_char);
}
}
}
builder.append_value(&csv_string);
}
}
Ok(Arc::new(builder.finish()))
}
#[inline]
fn escape_value(value: &str, quote_char: char, escape_char: char, output: &mut String) {
for ch in value.chars() {
if ch == quote_char || ch == escape_char {
output.push(escape_char);
}
output.push(ch);
}
}