Skip to content

Commit 476e787

Browse files
author
B Vadlamani
committed
setup_spark_based_cast_tests
1 parent 199e3f7 commit 476e787

1 file changed

Lines changed: 141 additions & 33 deletions

File tree

  • datafusion/spark/src/function/conversion

datafusion/spark/src/function/conversion/cast.rs

Lines changed: 141 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ use std::sync::Arc;
3333

3434
const MICROS_PER_SECOND: i64 = 1_000_000;
3535

36-
/// Spark-compatible `cast` function for type conversions.
36+
/// Convert seconds to microseconds with saturating overflow behavior
37+
fn secs_to_micros(secs: impl Into<i64>) -> i64 {
38+
secs.into().saturating_mul(MICROS_PER_SECOND)
39+
}
40+
41+
/// Spark-compatible `cast` function for type conversions
3742
///
38-
/// This implements Spark's CAST expression with a target type parameter.
43+
/// This implements Spark's CAST expression with a target type parameter
3944
///
4045
/// # Usage
4146
/// ```sql
@@ -46,11 +51,11 @@ const MICROS_PER_SECOND: i64 = 1_000_000;
4651
/// - Int8/Int16/Int32/Int64 -> Timestamp (target_type = 'timestamp')
4752
///
4853
/// The integer value is interpreted as seconds since the Unix epoch (1970-01-01 00:00:00 UTC)
49-
/// and converted to a timestamp with microsecond precision.
54+
/// and converted to a timestamp with microsecond precision (matches spark's spec)
5055
///
5156
/// # Overflow behavior
5257
/// Uses saturating multiplication to handle overflow - values that would overflow
53-
/// i64 when multiplied by 1,000,000 are clamped to i64::MAX or i64::MIN.
58+
/// i64 when multiplied by 1,000,000 are clamped to i64::MAX or i64::MIN
5459
///
5560
/// # References
5661
/// - <https://spark.apache.org/docs/latest/api/sql/index.html#cast>
@@ -81,7 +86,7 @@ impl SparkCast {
8186
/// Parse target type string into a DataType
8287
fn parse_target_type(type_str: &str) -> DataFusionResult<DataType> {
8388
match type_str.to_lowercase().as_str() {
84-
// could add further data type support in future
89+
// further data type support in future
8590
"timestamp" => Ok(DataType::Timestamp(TimeUnit::Microsecond, None)),
8691
other => exec_err!(
8792
"Unsupported spark_cast target type '{}'. Supported types: timestamp",
@@ -108,6 +113,7 @@ fn get_target_type_from_scalar_args(
108113

109114
fn cast_int_to_timestamp<T: ArrowPrimitiveType>(
110115
array: &ArrayRef,
116+
timezone: Option<Arc<str>>,
111117
) -> DataFusionResult<ArrayRef>
112118
where
113119
T::Native: Into<i64>,
@@ -120,12 +126,12 @@ where
120126
builder.append_null();
121127
} else {
122128
// spark saturates to i64 min/max
123-
let micros = (arr.value(i).into()).saturating_mul(MICROS_PER_SECOND);
129+
let micros = secs_to_micros(arr.value(i).into());
124130
builder.append_value(micros);
125131
}
126132
}
127133

128-
Ok(Arc::new(builder.finish()))
134+
Ok(Arc::new(builder.finish().with_timezone_opt(timezone)))
129135
}
130136

131137
impl ScalarUDFImpl for SparkCast {
@@ -159,35 +165,47 @@ impl ScalarUDFImpl for SparkCast {
159165
args: ScalarFunctionArgs,
160166
) -> DataFusionResult<ColumnarValue> {
161167
let target_type = args.return_field.data_type();
168+
// Use session timezone, fallback to UTC if not set
169+
let session_tz: Arc<str> = args
170+
.config_options
171+
.execution
172+
.time_zone
173+
.clone()
174+
.map(|s| Arc::from(s.as_str()))
175+
.unwrap_or_else(|| Arc::from("UTC"));
162176

163177
match target_type {
164178
DataType::Timestamp(TimeUnit::Microsecond, None) => {
165-
cast_to_timestamp(&args.args[0])
179+
cast_to_timestamp(&args.args[0], Some(session_tz))
166180
}
167181
other => exec_err!("Unsupported spark_cast target type: {:?}", other),
168182
}
169183
}
170184
}
171185

172-
/// Cast value to timestamp
173-
fn cast_to_timestamp(input: &ColumnarValue) -> DataFusionResult<ColumnarValue> {
186+
/// Cast value to timestamp internal function
187+
fn cast_to_timestamp(
188+
input: &ColumnarValue,
189+
timezone: Option<Arc<str>>,
190+
) -> DataFusionResult<ColumnarValue> {
174191
match input {
175192
ColumnarValue::Array(array) => match array.data_type() {
176193
DataType::Null => Ok(ColumnarValue::Array(Arc::new(
177-
arrow::array::TimestampMicrosecondArray::new_null(array.len()),
194+
arrow::array::TimestampMicrosecondArray::new_null(array.len())
195+
.with_timezone_opt(timezone),
178196
))),
179197
DataType::Int8 => Ok(ColumnarValue::Array(
180-
cast_int_to_timestamp::<Int8Type>(array)?,
198+
cast_int_to_timestamp::<Int8Type>(array, timezone)?,
181199
)),
182200
DataType::Int16 => Ok(ColumnarValue::Array(cast_int_to_timestamp::<
183201
Int16Type,
184-
>(array)?)),
202+
>(array, timezone)?)),
185203
DataType::Int32 => Ok(ColumnarValue::Array(cast_int_to_timestamp::<
186204
Int32Type,
187-
>(array)?)),
205+
>(array, timezone)?)),
188206
DataType::Int64 => Ok(ColumnarValue::Array(cast_int_to_timestamp::<
189207
Int64Type,
190-
>(array)?)),
208+
>(array, timezone)?)),
191209
other => exec_err!("Unsupported cast from {:?} to timestamp", other),
192210
},
193211
ColumnarValue::Scalar(scalar) => {
@@ -197,22 +215,16 @@ fn cast_to_timestamp(input: &ColumnarValue) -> DataFusionResult<ColumnarValue> {
197215
| ScalarValue::Int16(None)
198216
| ScalarValue::Int32(None)
199217
| ScalarValue::Int64(None) => None,
200-
ScalarValue::Int8(Some(v)) => {
201-
Some((*v as i64).saturating_mul(MICROS_PER_SECOND))
202-
}
203-
ScalarValue::Int16(Some(v)) => {
204-
Some((*v as i64).saturating_mul(MICROS_PER_SECOND))
205-
}
206-
ScalarValue::Int32(Some(v)) => {
207-
Some((*v as i64).saturating_mul(MICROS_PER_SECOND))
208-
}
209-
ScalarValue::Int64(Some(v)) => Some(v.saturating_mul(MICROS_PER_SECOND)),
218+
ScalarValue::Int8(Some(v)) => Some(secs_to_micros(*v)),
219+
ScalarValue::Int16(Some(v)) => Some(secs_to_micros(*v)),
220+
ScalarValue::Int32(Some(v)) => Some(secs_to_micros(*v)),
221+
ScalarValue::Int64(Some(v)) => Some(secs_to_micros(*v)),
210222
other => {
211223
return exec_err!("Unsupported cast from {:?} to timestamp", other);
212224
}
213225
};
214226
Ok(ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(
215-
micros, None,
227+
micros, timezone,
216228
)))
217229
}
218230
}
@@ -224,12 +236,25 @@ mod tests {
224236
use arrow::array::{Int8Array, Int16Array, Int32Array, Int64Array};
225237
use arrow::datatypes::TimestampMicrosecondType;
226238

239+
// helpers to make testing easier
227240
fn make_args(input: ColumnarValue, target_type: &str) -> ScalarFunctionArgs {
241+
make_args_with_timezone(input, target_type, None)
242+
}
243+
244+
fn make_args_with_timezone(
245+
input: ColumnarValue,
246+
target_type: &str,
247+
timezone: Option<&str>,
248+
) -> ScalarFunctionArgs {
228249
let return_field = Arc::new(Field::new(
229250
"result",
230251
DataType::Timestamp(TimeUnit::Microsecond, None),
231252
true,
232253
));
254+
let mut config = datafusion_common::config::ConfigOptions::default();
255+
if let Some(tz) = timezone {
256+
config.execution.time_zone = Some(tz.to_string());
257+
}
233258
ScalarFunctionArgs {
234259
args: vec![
235260
input,
@@ -238,24 +263,46 @@ mod tests {
238263
arg_fields: vec![],
239264
number_rows: 0,
240265
return_field,
241-
config_options: Arc::new(Default::default()),
266+
config_options: Arc::new(config),
242267
}
243268
}
244269

245270
fn assert_scalar_timestamp(result: ColumnarValue, expected: i64) {
271+
assert_scalar_timestamp_with_tz(result, expected, "UTC");
272+
}
273+
274+
fn assert_scalar_timestamp_with_tz(
275+
result: ColumnarValue,
276+
expected: i64,
277+
expected_tz: &str,
278+
) {
246279
match result {
247-
ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(Some(val), None)) => {
280+
ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(
281+
Some(val),
282+
Some(tz),
283+
)) => {
248284
assert_eq!(val, expected);
285+
assert_eq!(tz.as_ref(), expected_tz);
286+
}
287+
_ => {
288+
panic!(
289+
"Expected scalar timestamp with value {expected} and {expected_tz} timezone"
290+
)
249291
}
250-
_ => panic!("Expected scalar timestamp with value {expected}"),
251292
}
252293
}
253294

254295
fn assert_scalar_null(result: ColumnarValue) {
255-
assert!(matches!(
256-
result,
257-
ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(None, None))
258-
));
296+
assert_scalar_null_with_tz(result, "UTC");
297+
}
298+
299+
fn assert_scalar_null_with_tz(result: ColumnarValue, expected_tz: &str) {
300+
match result {
301+
ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(None, Some(tz))) => {
302+
assert_eq!(tz.as_ref(), expected_tz);
303+
}
304+
_ => panic!("Expected null scalar timestamp with {expected_tz} timezone"),
305+
}
259306
}
260307

261308
#[test]
@@ -521,4 +568,65 @@ mod tests {
521568
_ => panic!("Expected array result"),
522569
}
523570
}
571+
572+
#[test]
573+
fn test_cast_int_to_timestamp_with_timezones() {
574+
// Test with various timezones like Comet does
575+
let timezones = [
576+
"UTC",
577+
"America/New_York",
578+
"America/Los_Angeles",
579+
"Europe/London",
580+
"Asia/Tokyo",
581+
"Australia/Sydney",
582+
];
583+
584+
let cast = SparkCast::new();
585+
let test_value: i64 = 1704067200; // 2024-01-01 00:00:00 UTC
586+
let expected_micros = test_value * MICROS_PER_SECOND;
587+
588+
for tz in timezones {
589+
// scalar
590+
let args = make_args_with_timezone(
591+
ColumnarValue::Scalar(ScalarValue::Int64(Some(test_value))),
592+
"timestamp",
593+
Some(tz),
594+
);
595+
let result = cast.invoke_with_args(args).unwrap();
596+
assert_scalar_timestamp_with_tz(result, expected_micros, tz);
597+
598+
// array input
599+
let array: ArrayRef =
600+
Arc::new(Int64Array::from(vec![Some(test_value), None]));
601+
let args = make_args_with_timezone(
602+
ColumnarValue::Array(array),
603+
"timestamp",
604+
Some(tz),
605+
);
606+
let result = cast.invoke_with_args(args).unwrap();
607+
608+
match result {
609+
ColumnarValue::Array(result_array) => {
610+
let ts_array =
611+
result_array.as_primitive::<TimestampMicrosecondType>();
612+
assert_eq!(ts_array.value(0), expected_micros);
613+
assert!(ts_array.is_null(1));
614+
assert_eq!(ts_array.timezone(), Some(tz));
615+
}
616+
_ => panic!("Expected array result for timezone {tz}"),
617+
}
618+
}
619+
}
620+
621+
#[test]
622+
fn test_cast_int_to_timestamp_default_timezone() {
623+
let cast = SparkCast::new();
624+
let args = make_args(
625+
ColumnarValue::Scalar(ScalarValue::Int64(Some(0))),
626+
"timestamp",
627+
);
628+
let result = cast.invoke_with_args(args).unwrap();
629+
// Defaults to UTC
630+
assert_scalar_timestamp_with_tz(result, 0, "UTC");
631+
}
524632
}

0 commit comments

Comments
 (0)