diff --git a/docs/source/user-guide/latest/compatibility.md b/docs/source/user-guide/latest/compatibility.md index 60e2234f59..2cafe2a640 100644 --- a/docs/source/user-guide/latest/compatibility.md +++ b/docs/source/user-guide/latest/compatibility.md @@ -159,6 +159,9 @@ The following cast operations are generally compatible with Spark except for the | string | short | | | string | integer | | | string | long | | +| string | float | | +| string | double | | +| string | decimal | | | string | binary | | | string | date | Only supports years between 262143 BC and 262142 AD | | binary | string | | @@ -181,9 +184,6 @@ The following cast operations are not compatible with Spark for all inputs and a |-|-|-| | float | decimal | There can be rounding differences | | double | decimal | There can be rounding differences | -| string | float | Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. Does not support ANSI mode. | -| string | double | Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. Does not support ANSI mode. | -| string | decimal | Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. Does not support ANSI mode. Returns 0.0 instead of null if input contains no digits | | string | timestamp | Not all valid formats are supported | diff --git a/native/spark-expr/src/conversion_funcs/cast.rs b/native/spark-expr/src/conversion_funcs/cast.rs index 12a147c6e1..ea9993771e 100644 --- a/native/spark-expr/src/conversion_funcs/cast.rs +++ b/native/spark-expr/src/conversion_funcs/cast.rs @@ -20,12 +20,13 @@ use crate::{timezone, BinaryOutputStyle}; use crate::{EvalMode, SparkError, SparkResult}; use arrow::array::builder::StringBuilder; use arrow::array::{ - BooleanBuilder, Decimal128Builder, DictionaryArray, GenericByteArray, ListArray, StringArray, - StructArray, + ArrayAccessor, BooleanBuilder, Decimal128Builder, DictionaryArray, GenericByteArray, ListArray, + PrimitiveBuilder, StringArray, StructArray, }; use arrow::compute::can_cast_types; use arrow::datatypes::{ - ArrowDictionaryKeyType, ArrowNativeType, DataType, GenericBinaryType, Schema, + i256, ArrowDictionaryKeyType, ArrowNativeType, DataType, Decimal256Type, GenericBinaryType, + Schema, }; use arrow::{ array::{ @@ -44,6 +45,7 @@ use arrow::{ record_batch::RecordBatch, util::display::FormatOptions, }; +use base64::prelude::*; use chrono::{DateTime, NaiveDate, TimeZone, Timelike}; use datafusion::common::{ cast::as_generic_string_array, internal_err, DataFusionError, Result as DataFusionResult, @@ -65,8 +67,6 @@ use std::{ sync::Arc, }; -use base64::prelude::*; - static TIMESTAMP_FORMAT: Option<&str> = Some("%Y-%m-%d %H:%M:%S%.f"); const MICROS_PER_SECOND: i64 = 1000000; @@ -216,19 +216,9 @@ fn can_cast_from_string(to_type: &DataType, options: &SparkCastOptions) -> bool use DataType::*; match to_type { Boolean | Int8 | Int16 | Int32 | Int64 | Binary => true, - Float32 | Float64 => { - // https://github.com/apache/datafusion-comet/issues/326 - // Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. - // Does not support ANSI mode. - options.allow_incompat - } - Decimal128(_, _) => { - // https://github.com/apache/datafusion-comet/issues/325 - // Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. - // Does not support ANSI mode. Returns 0.0 instead of null if input contains no digits - - options.allow_incompat - } + Float32 | Float64 => true, + Decimal128(_, _) => true, + Decimal256(_, _) => true, Date32 | Date64 => { // https://github.com/apache/datafusion-comet/issues/327 // Only supports years between 262143 BC and 262142 AD @@ -976,6 +966,13 @@ fn cast_array( cast_string_to_timestamp(&array, to_type, eval_mode, &cast_options.timezone) } (Utf8, Date32) => cast_string_to_date(&array, to_type, eval_mode), + (Utf8, Float16 | Float32 | Float64) => cast_string_to_float(&array, to_type, eval_mode), + (Utf8 | LargeUtf8, Decimal128(precision, scale)) => { + cast_string_to_decimal(&array, to_type, precision, scale, eval_mode) + } + (Utf8 | LargeUtf8, Decimal256(precision, scale)) => { + cast_string_to_decimal(&array, to_type, precision, scale, eval_mode) + } (Int64, Int32) | (Int64, Int16) | (Int64, Int8) @@ -1058,6 +1055,441 @@ fn cast_array( Ok(spark_cast_postprocess(cast_result?, from_type, to_type)) } +fn cast_string_to_decimal( + array: &ArrayRef, + to_type: &DataType, + precision: &u8, + scale: &i8, + eval_mode: EvalMode, +) -> SparkResult { + match to_type { + DataType::Decimal128(_, _) => { + cast_string_to_decimal128_impl(array, eval_mode, *precision, *scale) + } + DataType::Decimal256(_, _) => { + cast_string_to_decimal256_impl(array, eval_mode, *precision, *scale) + } + _ => Err(SparkError::Internal(format!( + "Unexpected type in cast_string_to_decimal: {:?}", + to_type + ))), + } +} + +fn cast_string_to_decimal128_impl( + array: &ArrayRef, + eval_mode: EvalMode, + precision: u8, + scale: i8, +) -> SparkResult { + let string_array = array + .as_any() + .downcast_ref::() + .ok_or_else(|| SparkError::Internal("Expected string array".to_string()))?; + + let mut decimal_builder = Decimal128Builder::with_capacity(string_array.len()); + + for i in 0..string_array.len() { + if string_array.is_null(i) { + decimal_builder.append_null(); + } else { + let str_value = string_array.value(i).trim(); + match parse_string_to_decimal(str_value, precision, scale) { + Ok(Some(decimal_value)) => { + decimal_builder.append_value(decimal_value); + } + Ok(None) => { + if eval_mode == EvalMode::Ansi { + return Err(invalid_value( + string_array.value(i), + "STRING", + &format!("DECIMAL({},{})", precision, scale), + )); + } + decimal_builder.append_null(); + } + Err(e) => { + if eval_mode == EvalMode::Ansi { + return Err(e); + } + decimal_builder.append_null(); + } + } + } + } + + Ok(Arc::new( + decimal_builder + .with_precision_and_scale(precision, scale)? + .finish(), + )) +} + +fn cast_string_to_decimal256_impl( + array: &ArrayRef, + eval_mode: EvalMode, + precision: u8, + scale: i8, +) -> SparkResult { + let string_array = array + .as_any() + .downcast_ref::() + .ok_or_else(|| SparkError::Internal("Expected string array".to_string()))?; + + let mut decimal_builder = PrimitiveBuilder::::with_capacity(string_array.len()); + + for i in 0..string_array.len() { + if string_array.is_null(i) { + decimal_builder.append_null(); + } else { + let str_value = string_array.value(i).trim(); + match parse_string_to_decimal(str_value, precision, scale) { + Ok(Some(decimal_value)) => { + // Convert i128 to i256 + let i256_value = i256::from_i128(decimal_value); + decimal_builder.append_value(i256_value); + } + Ok(None) => { + if eval_mode == EvalMode::Ansi { + return Err(invalid_value( + str_value, + "STRING", + &format!("DECIMAL({},{})", precision, scale), + )); + } + decimal_builder.append_null(); + } + Err(e) => { + if eval_mode == EvalMode::Ansi { + return Err(e); + } + decimal_builder.append_null(); + } + } + } + } + + Ok(Arc::new( + decimal_builder + .with_precision_and_scale(precision, scale)? + .finish(), + )) +} + +/// Validates if a string is a valid decimal similar to BigDecimal +fn is_valid_decimal_format(s: &str) -> bool { + if s.is_empty() { + return false; + } + + let bytes = s.as_bytes(); + let mut idx = 0; + let len = bytes.len(); + + // Skip leading +/- signs + if bytes[idx] == b'+' || bytes[idx] == b'-' { + idx += 1; + if idx >= len { + // Sign only. Fail early + return false; + } + } + + // Check invalid cases like "++", "+-" + if bytes[idx] == b'+' || bytes[idx] == b'-' { + return false; + } + + // Now we need at least one digit either before or after a decimal point + let mut has_digit = false; + let mut is_decimal_point_seen = false; + + while idx < len { + let ch = bytes[idx]; + + if ch.is_ascii_digit() { + has_digit = true; + idx += 1; + } else if ch == b'.' { + if is_decimal_point_seen { + // Multiple decimal points or decimal after exponent + return false; + } + is_decimal_point_seen = true; + idx += 1; + } else if ch.eq_ignore_ascii_case(&b'e') { + if !has_digit { + // Exponent without any digits before it + return false; + } + idx += 1; + // Exponent part must have optional sign followed by atleast a digit + if idx >= len { + return false; + } + + if bytes[idx] == b'+' || bytes[idx] == b'-' { + idx += 1; + if idx >= len { + return false; + } + } + + // Must have at least one digit in exponent + if !bytes[idx].is_ascii_digit() { + return false; + } + + // Rest all should only be digits + while idx < len { + if !bytes[idx].is_ascii_digit() { + return false; + } + idx += 1; + } + break; + } else { + // Invalid character found. Fail fast + return false; + } + } + has_digit +} + +/// Parse a string to decimal following Spark's behavior +fn parse_string_to_decimal(s: &str, precision: u8, scale: i8) -> SparkResult> { + if s.is_empty() { + return Ok(None); + } + // Handle special values (inf, nan, etc.) + if s.eq_ignore_ascii_case("inf") + || s.eq_ignore_ascii_case("+inf") + || s.eq_ignore_ascii_case("infinity") + || s.eq_ignore_ascii_case("+infinity") + || s.eq_ignore_ascii_case("-inf") + || s.eq_ignore_ascii_case("-infinity") + || s.eq_ignore_ascii_case("nan") + { + return Ok(None); + } + + if !is_valid_decimal_format(s) { + return Ok(None); + } + + match parse_decimal_str(s) { + Ok((mantissa, exponent)) => { + // Convert to target scale + let target_scale = scale as i32; + let scale_adjustment = target_scale - exponent; + + let scaled_value = if scale_adjustment >= 0 { + // Need to multiply (increase scale) but return None if scale is too high to fit i128 + if scale_adjustment > 38 { + return Ok(None); + } + mantissa.checked_mul(10_i128.pow(scale_adjustment as u32)) + } else { + // Need to multiply (increase scale) but return None if scale is too high to fit i128 + let abs_scale_adjustment = (-scale_adjustment) as u32; + if abs_scale_adjustment > 38 { + return Ok(Some(0)); + } + + let divisor = 10_i128.pow(abs_scale_adjustment); + let quotient_opt = mantissa.checked_div(divisor); + // Check if divisor is 0 + if quotient_opt.is_none() { + return Ok(None); + } + let quotient = quotient_opt.unwrap(); + let remainder = mantissa % divisor; + + // Round half up: if abs(remainder) >= divisor/2, round away from zero + let half_divisor = divisor / 2; + let rounded = if remainder.abs() >= half_divisor { + if mantissa >= 0 { + quotient + 1 + } else { + quotient - 1 + } + } else { + quotient + }; + Some(rounded) + }; + + match scaled_value { + Some(value) => { + // Check if it fits target precision + if is_validate_decimal_precision(value, precision) { + Ok(Some(value)) + } else { + Ok(None) + } + } + None => { + // Overflow while scaling + Ok(None) + } + } + } + Err(_) => Ok(None), + } +} + +/// Parse a decimal string into mantissa and scale +/// e.g., "123.45" -> (12345, 2), "-0.001" -> (-1, 3) +fn parse_decimal_str(s: &str) -> Result<(i128, i32), String> { + let s = s.trim(); + if s.is_empty() { + return Err("Empty string".to_string()); + } + + let (mantissa_str, exponent) = if let Some(e_pos) = s.find(|c| ['e', 'E'].contains(&c)) { + let mantissa_part = &s[..e_pos]; + let exponent_part = &s[e_pos + 1..]; + // Parse exponent + let exp: i32 = exponent_part + .parse() + .map_err(|e| format!("Invalid exponent: {}", e))?; + + (mantissa_part, exp) + } else { + (s, 0) + }; + + let negative = mantissa_str.starts_with('-'); + let mantissa_str = if negative || mantissa_str.starts_with('+') { + &mantissa_str[1..] + } else { + mantissa_str + }; + + let split_by_dot: Vec<&str> = mantissa_str.split('.').collect(); + + if split_by_dot.len() > 2 { + return Err("Multiple decimal points".to_string()); + } + + let integral_part = split_by_dot[0]; + let fractional_part = if split_by_dot.len() == 2 { + split_by_dot[1] + } else { + "" + }; + + // Parse integral part + let integral_value: i128 = if integral_part.is_empty() { + // Empty integral part is valid (e.g., ".5" or "-.7e9") + 0 + } else { + integral_part + .parse() + .map_err(|_| "Invalid integral part".to_string())? + }; + + // Parse fractional part + let fractional_scale = fractional_part.len() as i32; + let fractional_value: i128 = if fractional_part.is_empty() { + 0 + } else { + fractional_part + .parse() + .map_err(|_| "Invalid fractional part".to_string())? + }; + + // Combine: value = integral * 10^fractional_scale + fractional + let mantissa = integral_value + .checked_mul(10_i128.pow(fractional_scale as u32)) + .and_then(|v| v.checked_add(fractional_value)) + .ok_or("Overflow in mantissa calculation")?; + + let final_mantissa = if negative { -mantissa } else { mantissa }; + // final scale = fractional_scale - exponent + // For example : "1.23E-5" has fractional_scale=2, exponent=-5, so scale = 2 - (-5) = 7 + let final_scale = fractional_scale - exponent; + Ok((final_mantissa, final_scale)) +} + +fn cast_string_to_float( + array: &ArrayRef, + to_type: &DataType, + eval_mode: EvalMode, +) -> SparkResult { + match to_type { + DataType::Float16 | DataType::Float32 => { + cast_string_to_float_impl::(array, eval_mode, "FLOAT") + } + DataType::Float64 => cast_string_to_float_impl::(array, eval_mode, "DOUBLE"), + _ => Err(SparkError::Internal(format!( + "Unsupported cast to float type: {:?}", + to_type + ))), + } +} + +fn cast_string_to_float_impl( + array: &ArrayRef, + eval_mode: EvalMode, + type_name: &str, +) -> SparkResult +where + T::Native: FromStr + num::Float, +{ + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| SparkError::Internal("Expected string array".to_string()))?; + + let mut builder = PrimitiveBuilder::::with_capacity(arr.len()); + + for i in 0..arr.len() { + if arr.is_null(i) { + builder.append_null(); + } else { + let str_value = arr.value(i).trim(); + match parse_string_to_float(str_value) { + Some(v) => builder.append_value(v), + None => { + if eval_mode == EvalMode::Ansi { + return Err(invalid_value(arr.value(i), "STRING", type_name)); + } + builder.append_null(); + } + } + } + } + + Ok(Arc::new(builder.finish())) +} + +/// helper to parse floats from string inputs +fn parse_string_to_float(s: &str) -> Option +where + F: FromStr + num::Float, +{ + let s_lower = s.to_lowercase(); + // Handle +inf / -inf + if s_lower == "inf" || s_lower == "+inf" || s_lower == "infinity" || s_lower == "+infinity" { + return Some(F::infinity()); + } + if s_lower == "-inf" || s_lower == "-infinity" { + return Some(F::neg_infinity()); + } + if s_lower == "nan" { + return Some(F::nan()); + } + // Remove D/F suffix if present + let pruned_float_str = if s_lower.ends_with('d') || s_lower.ends_with('f') { + &s[..s.len() - 1] + } else { + s + }; + // Rust's parse logic already handles scientific notations so we just rely on it + pruned_float_str.parse::().ok() +} + fn cast_binary_to_string( array: &dyn Array, spark_cast_options: &SparkCastOptions, @@ -1185,11 +1617,13 @@ fn is_datafusion_spark_compatible( | DataType::Decimal256(_, _) | DataType::Utf8 // note that there can be formatting differences ), - DataType::Utf8 if allow_incompat => matches!( + DataType::Utf8 if allow_incompat => { + matches!(to_type, DataType::Binary | DataType::Decimal128(_, _)) + } + DataType::Utf8 => matches!( to_type, - DataType::Binary | DataType::Float32 | DataType::Float64 | DataType::Decimal128(_, _) + DataType::Binary | DataType::Float32 | DataType::Float64 ), - DataType::Utf8 => matches!(to_type, DataType::Binary), DataType::Date32 => matches!(to_type, DataType::Utf8), DataType::Timestamp(_, _) => { matches!( diff --git a/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala b/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala index 98ce8ac44d..4b16242305 100644 --- a/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala +++ b/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala @@ -185,16 +185,9 @@ object CometCast extends CometExpressionSerde[Cast] with CometExprShim { case DataTypes.BinaryType => Compatible() case DataTypes.FloatType | DataTypes.DoubleType => - // https://github.com/apache/datafusion-comet/issues/326 - Incompatible( - Some( - "Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. " + - "Does not support ANSI mode.")) + Compatible() case _: DecimalType => - // https://github.com/apache/datafusion-comet/issues/325 - Incompatible( - Some("Does not support inputs ending with 'd' or 'f'. Does not support 'inf'. " + - "Does not support ANSI mode. Returns 0.0 instead of null if input contains no digits")) + Compatible() case DataTypes.DateType => // https://github.com/apache/datafusion-comet/issues/327 Compatible(Some("Only supports years between 262143 BC and 262142 AD")) diff --git a/spark/src/test/scala/org/apache/comet/CometCastSuite.scala b/spark/src/test/scala/org/apache/comet/CometCastSuite.scala index 1912e982b9..36364b801b 100644 --- a/spark/src/test/scala/org/apache/comet/CometCastSuite.scala +++ b/spark/src/test/scala/org/apache/comet/CometCastSuite.scala @@ -109,6 +109,32 @@ class CometCastSuite extends CometTestBase with AdaptiveSparkPlanHelper { assertTestsExist(CometCast.supportedTypes, CometCast.supportedTypes) } + val specialValues: Seq[String] = Seq( + "1.5f", + "1.5F", + "2.0d", + "2.0D", + "3.14159265358979d", + "inf", + "Inf", + "INF", + "+inf", + "+Infinity", + "-inf", + "-Infinity", + "NaN", + "nan", + "NAN", + "1.23e4", + "1.23E4", + "-1.23e-4", + " 123.456789 ", + "0.0", + "-0.0", + "", + "xyz", + null) + // CAST from BooleanType test("cast BooleanType to ByteType") { @@ -652,54 +678,130 @@ class CometCastSuite extends CometTestBase with AdaptiveSparkPlanHelper { castTest(gen.generateStrings(dataSize, numericPattern, 8).toDF("a"), DataTypes.LongType) } - ignore("cast StringType to FloatType") { - // https://github.com/apache/datafusion-comet/issues/326 - castTest(gen.generateStrings(dataSize, numericPattern, 8).toDF("a"), DataTypes.FloatType) + test("cast StringType to FloatType special values") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + Seq(true, false).foreach { v => + castTest(specialValues.toDF("a"), DataTypes.FloatType, testAnsi = v) + } } - test("cast StringType to FloatType (partial support)") { - withSQLConf( - CometConf.getExprAllowIncompatConfigKey(classOf[Cast]) -> "true", - SQLConf.ANSI_ENABLED.key -> "false") { - castTest( - gen.generateStrings(dataSize, "0123456789.", 8).toDF("a"), - DataTypes.FloatType, - testAnsi = false) + test("cast StringType to DoubleType special values") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + Seq(true, false).foreach { v => + castTest(specialValues.toDF("a"), DataTypes.DoubleType, testAnsi = v) } } - ignore("cast StringType to DoubleType") { - // https://github.com/apache/datafusion-comet/issues/326 - castTest(gen.generateStrings(dataSize, numericPattern, 8).toDF("a"), DataTypes.DoubleType) + test("cast StringType to DoubleType") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + Seq(true, false).foreach { v => + castTest( + gen.generateStrings(dataSize, numericPattern, 10).toDF("a"), + DataTypes.DoubleType, + testAnsi = v) + } } - test("cast StringType to DoubleType (partial support)") { - withSQLConf( - CometConf.getExprAllowIncompatConfigKey(classOf[Cast]) -> "true", - SQLConf.ANSI_ENABLED.key -> "false") { + test("cast StringType to FloatType") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + Seq(true, false).foreach { v => castTest( - gen.generateStrings(dataSize, "0123456789.", 8).toDF("a"), - DataTypes.DoubleType, - testAnsi = false) + gen.generateStrings(dataSize, numericPattern, 10).toDF("a"), + DataTypes.FloatType, + testAnsi = v) } } - ignore("cast StringType to DecimalType(10,2)") { - // https://github.com/apache/datafusion-comet/issues/325 - val values = gen.generateStrings(dataSize, numericPattern, 8).toDF("a") - castTest(values, DataTypes.createDecimalType(10, 2)) + test("cast StringType to Float type scientific notation") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + val values = Seq( + "1.23E-5", + "1.23e10", + "1.23E+10", + "-1.23e-5", + "1e5", + "1E-2", + "-1.5e3", + "1.23E0", + "0e0", + "1.23e", + "e5", + null).toDF("a") + Seq(true, false).foreach(k => castTest(values, DataTypes.FloatType, testAnsi = k)) + } + + test("cast StringType to DecimalType(22,2)") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + val values = gen.generateStrings(dataSize, numericPattern, 12).toDF("a") + Seq(true, false).foreach(k => + castTest(values, DataTypes.createDecimalType(22, 2), testAnsi = k)) } - test("cast StringType to DecimalType(10,2) (partial support)") { - withSQLConf( - CometConf.getExprAllowIncompatConfigKey(classOf[Cast]) -> "true", - SQLConf.ANSI_ENABLED.key -> "false") { - val values = gen - .generateStrings(dataSize, "0123456789.", 8) - .filter(_.exists(_.isDigit)) - .toDF("a") - castTest(values, DataTypes.createDecimalType(10, 2), testAnsi = false) - } + test("cast StringType to DecimalType(2,2)") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + val values = gen.generateStrings(dataSize, numericPattern, 12).toDF("a") + Seq(true, false).foreach(k => + castTest(values, DataTypes.createDecimalType(2, 2), testAnsi = k)) + } + + test("cast StringType to DecimalType(38,10) high precision") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + val values = gen.generateStrings(dataSize, numericPattern, 38).toDF("a") + Seq(true, false).foreach(k => + castTest(values, DataTypes.createDecimalType(38, 10), testAnsi = k)) + } + + test("cast StringType to DecimalType(10,2) basic values") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + val values = Seq( + "123.45", + "-67.89", + "-67.89", + "-67.895", + "67.895", + "0.001", + "999.99", + "123.456", + "123.45D", + ".5", + "5.", + "+123.45", + " 123.45 ", + "inf", + "", + "abc", + null).toDF("a") + Seq(true, false).foreach(k => + castTest(values, DataTypes.createDecimalType(10, 2), testAnsi = k)) + } + + test("cast StringType to Decimal type scientific notation") { + // TODO fix for Spark 4.0.0 + assume(!isSpark40Plus) + val values = Seq( + "1.23E-5", + "1.23e10", + "1.23E+10", + "-1.23e-5", + "1e5", + "1E-2", + "-1.5e3", + "1.23E0", + "0e0", + "1.23e", + "e5", + null).toDF("a") + Seq(true, false).foreach(k => + castTest(values, DataTypes.createDecimalType(23, 8), testAnsi = k)) } test("cast StringType to BinaryType") {