Skip to main content

alopex_core/dataframe/
cast.rs

1//! Type casting primitives for DataFrame values.
2
3use std::fmt;
4use std::num::ParseFloatError;
5
6use crate::Error;
7use crate::Result;
8
9/// Stable cast failure code for values outside the target type range.
10pub const CAST_REASON_OVERFLOW: &str = "overflow";
11/// Stable cast failure code for conversions that would discard information.
12pub const CAST_REASON_PRECISION_LOSS: &str = "precision_loss";
13/// Stable cast failure code for malformed string input.
14pub const CAST_REASON_INVALID_FORMAT: &str = "invalid_format";
15/// Stable cast failure code for NaN or infinite floating-point input.
16pub const CAST_REASON_NON_FINITE: &str = "non_finite";
17/// Stable cast failure code for unsupported type pairs.
18pub const CAST_REASON_UNSUPPORTED: &str = "unsupported";
19
20const MICROS_PER_SECOND: i64 = 1_000_000;
21const SECONDS_PER_DAY: i64 = 86_400;
22
23/// DataFrame scalar types supported by the core cast matrix.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum DataType {
26    /// Absence of a value.
27    Null,
28    /// 32-bit signed integer.
29    Int32,
30    /// 64-bit signed integer.
31    Int64,
32    /// 32-bit floating point.
33    Float32,
34    /// 64-bit floating point.
35    Float64,
36    /// UTF-8 string.
37    Utf8,
38    /// Timestamp stored as microseconds since the Unix epoch.
39    TimestampMicros,
40}
41
42impl DataType {
43    /// Return the stable display name used in cast errors.
44    pub fn as_str(self) -> &'static str {
45        match self {
46            Self::Null => "Null",
47            Self::Int32 => "Int32",
48            Self::Int64 => "Int64",
49            Self::Float32 => "Float32",
50            Self::Float64 => "Float64",
51            Self::Utf8 => "Utf8",
52            Self::TimestampMicros => "TimestampMicros",
53        }
54    }
55}
56
57impl fmt::Display for DataType {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(self.as_str())
60    }
61}
62
63/// DataFrame scalar value used by the core cast primitives.
64#[derive(Clone, Debug, PartialEq)]
65pub enum DataValue {
66    /// Absence of a value.
67    Null,
68    /// 32-bit signed integer.
69    Int32(i32),
70    /// 64-bit signed integer.
71    Int64(i64),
72    /// 32-bit floating point.
73    Float32(f32),
74    /// 64-bit floating point.
75    Float64(f64),
76    /// UTF-8 string.
77    Utf8(String),
78    /// Timestamp stored as microseconds since the Unix epoch.
79    TimestampMicros(i64),
80}
81
82impl DataValue {
83    /// Return this value's DataFrame type.
84    pub fn data_type(&self) -> DataType {
85        match self {
86            Self::Null => DataType::Null,
87            Self::Int32(_) => DataType::Int32,
88            Self::Int64(_) => DataType::Int64,
89            Self::Float32(_) => DataType::Float32,
90            Self::Float64(_) => DataType::Float64,
91            Self::Utf8(_) => DataType::Utf8,
92            Self::TimestampMicros(_) => DataType::TimestampMicros,
93        }
94    }
95}
96
97/// Return whether the preferred cast matrix has a path between two types.
98pub fn can_cast(from: DataType, to: DataType) -> bool {
99    if from == to || from == DataType::Null {
100        return true;
101    }
102
103    matches!(
104        (from, to),
105        (_, DataType::Utf8)
106            | (
107                DataType::Utf8,
108                DataType::Int32
109                    | DataType::Int64
110                    | DataType::Float32
111                    | DataType::Float64
112                    | DataType::TimestampMicros,
113            )
114            | (
115                DataType::Int32 | DataType::Int64 | DataType::Float32 | DataType::Float64,
116                DataType::Int32
117                    | DataType::Int64
118                    | DataType::Float32
119                    | DataType::Float64
120                    | DataType::TimestampMicros,
121            )
122            | (
123                DataType::TimestampMicros,
124                DataType::Int32 | DataType::Int64 | DataType::Float32 | DataType::Float64,
125            )
126    )
127}
128
129/// Cast a DataFrame scalar value to the requested target type.
130pub fn cast_value(value: &DataValue, to_type: DataType) -> Result<DataValue> {
131    let from_type = value.data_type();
132    if !can_cast(from_type, to_type) {
133        return Err(cast_failed(from_type, to_type, CAST_REASON_UNSUPPORTED));
134    }
135
136    if matches!(value, DataValue::Null) {
137        return Ok(DataValue::Null);
138    }
139
140    if value.data_type() == to_type {
141        return Ok(value.clone());
142    }
143
144    match to_type {
145        DataType::Null => Ok(DataValue::Null),
146        DataType::Int32 => cast_to_int32(value),
147        DataType::Int64 => cast_to_int64(value),
148        DataType::Float32 => cast_to_float32(value),
149        DataType::Float64 => cast_to_float64(value),
150        DataType::Utf8 => Ok(DataValue::Utf8(value_to_string(value))),
151        DataType::TimestampMicros => cast_to_timestamp_micros(value),
152    }
153}
154
155fn cast_to_int32(value: &DataValue) -> Result<DataValue> {
156    let from_type = value.data_type();
157    let parsed = match value {
158        DataValue::Int64(v) => i32::try_from(*v)
159            .map_err(|_| cast_failed(from_type, DataType::Int32, CAST_REASON_OVERFLOW))?,
160        DataValue::Float32(v) => f64_to_i32(f64::from(*v), from_type)?,
161        DataValue::Float64(v) => f64_to_i32(*v, from_type)?,
162        DataValue::Utf8(v) => v
163            .trim()
164            .parse::<i32>()
165            .map_err(|_| cast_failed(from_type, DataType::Int32, CAST_REASON_INVALID_FORMAT))?,
166        DataValue::TimestampMicros(v) => {
167            micros_to_i64_epoch_seconds(*v, from_type).and_then(|seconds| {
168                i32::try_from(seconds)
169                    .map_err(|_| cast_failed(from_type, DataType::Int32, CAST_REASON_OVERFLOW))
170            })?
171        }
172        DataValue::Int32(v) => *v,
173        DataValue::Null => unreachable!("null is handled before typed casts"),
174    };
175    Ok(DataValue::Int32(parsed))
176}
177
178fn cast_to_int64(value: &DataValue) -> Result<DataValue> {
179    let from_type = value.data_type();
180    let parsed = match value {
181        DataValue::Int32(v) => i64::from(*v),
182        DataValue::Float32(v) => f64_to_i64(f64::from(*v), from_type)?,
183        DataValue::Float64(v) => f64_to_i64(*v, from_type)?,
184        DataValue::Utf8(v) => v
185            .trim()
186            .parse::<i64>()
187            .map_err(|_| cast_failed(from_type, DataType::Int64, CAST_REASON_INVALID_FORMAT))?,
188        DataValue::TimestampMicros(v) => micros_to_i64_epoch_seconds(*v, from_type)?,
189        DataValue::Int64(v) => *v,
190        DataValue::Null => unreachable!("null is handled before typed casts"),
191    };
192    Ok(DataValue::Int64(parsed))
193}
194
195fn cast_to_float32(value: &DataValue) -> Result<DataValue> {
196    let from_type = value.data_type();
197    let parsed = match value {
198        DataValue::Int32(v) => i64_to_f32(i64::from(*v), from_type)?,
199        DataValue::Int64(v) => i64_to_f32(*v, from_type)?,
200        DataValue::Float64(v) => f64_to_f32(*v, from_type)?,
201        DataValue::Utf8(v) => parse_f32(v.trim(), from_type)?,
202        DataValue::TimestampMicros(v) => {
203            f64_to_f32(micros_to_f64_epoch_seconds(*v, from_type)?, from_type)?
204        }
205        DataValue::Float32(v) => *v,
206        DataValue::Null => unreachable!("null is handled before typed casts"),
207    };
208    Ok(DataValue::Float32(parsed))
209}
210
211fn cast_to_float64(value: &DataValue) -> Result<DataValue> {
212    let from_type = value.data_type();
213    let parsed = match value {
214        DataValue::Int32(v) => f64::from(*v),
215        DataValue::Int64(v) => i64_to_f64(*v, from_type)?,
216        DataValue::Float32(v) => {
217            if v.is_finite() {
218                f64::from(*v)
219            } else {
220                return Err(cast_failed(
221                    from_type,
222                    DataType::Float64,
223                    CAST_REASON_NON_FINITE,
224                ));
225            }
226        }
227        DataValue::Utf8(v) => parse_f64(v.trim(), from_type)?,
228        DataValue::TimestampMicros(v) => micros_to_f64_epoch_seconds(*v, from_type)?,
229        DataValue::Float64(v) => *v,
230        DataValue::Null => unreachable!("null is handled before typed casts"),
231    };
232    Ok(DataValue::Float64(parsed))
233}
234
235fn cast_to_timestamp_micros(value: &DataValue) -> Result<DataValue> {
236    let from_type = value.data_type();
237    let micros = match value {
238        DataValue::Int32(v) => epoch_seconds_to_micros(i64::from(*v), from_type)?,
239        DataValue::Int64(v) => epoch_seconds_to_micros(*v, from_type)?,
240        DataValue::Float32(v) => f64_epoch_seconds_to_micros(f64::from(*v), from_type)?,
241        DataValue::Float64(v) => f64_epoch_seconds_to_micros(*v, from_type)?,
242        DataValue::Utf8(v) => parse_timestamp_micros(v.trim(), from_type)?,
243        DataValue::TimestampMicros(v) => *v,
244        DataValue::Null => unreachable!("null is handled before typed casts"),
245    };
246    Ok(DataValue::TimestampMicros(micros))
247}
248
249fn value_to_string(value: &DataValue) -> String {
250    match value {
251        DataValue::Null => "NULL".to_string(),
252        DataValue::Int32(v) => v.to_string(),
253        DataValue::Int64(v) => v.to_string(),
254        DataValue::Float32(v) => v.to_string(),
255        DataValue::Float64(v) => v.to_string(),
256        DataValue::Utf8(v) => v.clone(),
257        DataValue::TimestampMicros(v) => format_timestamp_micros(*v),
258    }
259}
260
261fn parse_f32(input: &str, from_type: DataType) -> Result<f32> {
262    let parsed = input
263        .parse::<f32>()
264        .map_err(|err| parse_float_error(from_type, DataType::Float32, err))?;
265    if parsed.is_finite() {
266        Ok(parsed)
267    } else {
268        Err(cast_failed(
269            from_type,
270            DataType::Float32,
271            CAST_REASON_NON_FINITE,
272        ))
273    }
274}
275
276fn parse_f64(input: &str, from_type: DataType) -> Result<f64> {
277    let parsed = input
278        .parse::<f64>()
279        .map_err(|err| parse_float_error(from_type, DataType::Float64, err))?;
280    if parsed.is_finite() {
281        Ok(parsed)
282    } else {
283        Err(cast_failed(
284            from_type,
285            DataType::Float64,
286            CAST_REASON_NON_FINITE,
287        ))
288    }
289}
290
291fn parse_float_error(from_type: DataType, to_type: DataType, _err: ParseFloatError) -> Error {
292    cast_failed(from_type, to_type, CAST_REASON_INVALID_FORMAT)
293}
294
295fn f64_to_i32(value: f64, from_type: DataType) -> Result<i32> {
296    ensure_finite(value, from_type, DataType::Int32)?;
297    if value.fract() != 0.0 {
298        return Err(cast_failed(
299            from_type,
300            DataType::Int32,
301            CAST_REASON_PRECISION_LOSS,
302        ));
303    }
304    if value < f64::from(i32::MIN) || value > f64::from(i32::MAX) {
305        return Err(cast_failed(
306            from_type,
307            DataType::Int32,
308            CAST_REASON_OVERFLOW,
309        ));
310    }
311    Ok(value as i32)
312}
313
314fn f64_to_i64(value: f64, from_type: DataType) -> Result<i64> {
315    ensure_finite(value, from_type, DataType::Int64)?;
316    if value.fract() != 0.0 {
317        return Err(cast_failed(
318            from_type,
319            DataType::Int64,
320            CAST_REASON_PRECISION_LOSS,
321        ));
322    }
323    if value < i64::MIN as f64 || value >= 9_223_372_036_854_775_808.0 {
324        return Err(cast_failed(
325            from_type,
326            DataType::Int64,
327            CAST_REASON_OVERFLOW,
328        ));
329    }
330    let casted = value as i64;
331    if casted as f64 != value {
332        return Err(cast_failed(
333            from_type,
334            DataType::Int64,
335            CAST_REASON_PRECISION_LOSS,
336        ));
337    }
338    Ok(casted)
339}
340
341fn f64_to_f32(value: f64, from_type: DataType) -> Result<f32> {
342    ensure_finite(value, from_type, DataType::Float32)?;
343    let casted = value as f32;
344    if !casted.is_finite() {
345        return Err(cast_failed(
346            from_type,
347            DataType::Float32,
348            CAST_REASON_OVERFLOW,
349        ));
350    }
351    if f64::from(casted) != value {
352        return Err(cast_failed(
353            from_type,
354            DataType::Float32,
355            CAST_REASON_PRECISION_LOSS,
356        ));
357    }
358    Ok(casted)
359}
360
361fn i64_to_f32(value: i64, from_type: DataType) -> Result<f32> {
362    if !integer_exact_in_float(value.unsigned_abs(), 24) {
363        return Err(cast_failed(
364            from_type,
365            DataType::Float32,
366            CAST_REASON_PRECISION_LOSS,
367        ));
368    }
369    Ok(value as f32)
370}
371
372fn i64_to_f64(value: i64, from_type: DataType) -> Result<f64> {
373    if !integer_exact_in_float(value.unsigned_abs(), 53) {
374        return Err(cast_failed(
375            from_type,
376            DataType::Float64,
377            CAST_REASON_PRECISION_LOSS,
378        ));
379    }
380    Ok(value as f64)
381}
382
383fn integer_exact_in_float(abs_value: u64, precision_bits: u32) -> bool {
384    if abs_value == 0 {
385        return true;
386    }
387    let bit_width = u64::BITS - abs_value.leading_zeros();
388    bit_width <= precision_bits || abs_value.trailing_zeros() >= bit_width - precision_bits
389}
390
391fn ensure_finite(value: f64, from_type: DataType, to_type: DataType) -> Result<()> {
392    if value.is_finite() {
393        Ok(())
394    } else {
395        Err(cast_failed(from_type, to_type, CAST_REASON_NON_FINITE))
396    }
397}
398
399fn epoch_seconds_to_micros(seconds: i64, from_type: DataType) -> Result<i64> {
400    seconds
401        .checked_mul(MICROS_PER_SECOND)
402        .ok_or_else(|| cast_failed(from_type, DataType::TimestampMicros, CAST_REASON_OVERFLOW))
403}
404
405fn f64_epoch_seconds_to_micros(seconds: f64, from_type: DataType) -> Result<i64> {
406    ensure_finite(seconds, from_type, DataType::TimestampMicros)?;
407    let micros = seconds * MICROS_PER_SECOND as f64;
408    ensure_finite(micros, from_type, DataType::TimestampMicros)?;
409    if micros.fract() != 0.0 {
410        return Err(cast_failed(
411            from_type,
412            DataType::TimestampMicros,
413            CAST_REASON_PRECISION_LOSS,
414        ));
415    }
416    if micros < i64::MIN as f64 || micros >= 9_223_372_036_854_775_808.0 {
417        return Err(cast_failed(
418            from_type,
419            DataType::TimestampMicros,
420            CAST_REASON_OVERFLOW,
421        ));
422    }
423    Ok(micros as i64)
424}
425
426fn micros_to_i64_epoch_seconds(micros: i64, from_type: DataType) -> Result<i64> {
427    if micros % MICROS_PER_SECOND != 0 {
428        return Err(cast_failed(
429            from_type,
430            DataType::Int64,
431            CAST_REASON_PRECISION_LOSS,
432        ));
433    }
434    Ok(micros / MICROS_PER_SECOND)
435}
436
437fn micros_to_f64_epoch_seconds(micros: i64, from_type: DataType) -> Result<f64> {
438    let micros = i64_to_f64(micros, from_type)?;
439    Ok(micros / MICROS_PER_SECOND as f64)
440}
441
442fn parse_timestamp_micros(input: &str, from_type: DataType) -> Result<i64> {
443    let input = input.strip_suffix('Z').unwrap_or(input);
444    let (date, time) = input
445        .split_once('T')
446        .or_else(|| input.split_once(' '))
447        .ok_or_else(|| {
448            cast_failed(
449                from_type,
450                DataType::TimestampMicros,
451                CAST_REASON_INVALID_FORMAT,
452            )
453        })?;
454
455    let mut date_parts = date.split('-');
456    let year = parse_date_part::<i32>(date_parts.next(), from_type)?;
457    let month = parse_date_part::<u32>(date_parts.next(), from_type)?;
458    let day = parse_date_part::<u32>(date_parts.next(), from_type)?;
459    if date_parts.next().is_some() || !valid_date(year, month, day) {
460        return Err(cast_failed(
461            from_type,
462            DataType::TimestampMicros,
463            CAST_REASON_INVALID_FORMAT,
464        ));
465    }
466
467    let mut time_parts = time.split(':');
468    let hour = parse_date_part::<u32>(time_parts.next(), from_type)?;
469    let minute = parse_date_part::<u32>(time_parts.next(), from_type)?;
470    let second_text = time_parts.next().ok_or_else(|| {
471        cast_failed(
472            from_type,
473            DataType::TimestampMicros,
474            CAST_REASON_INVALID_FORMAT,
475        )
476    })?;
477    if time_parts.next().is_some() {
478        return Err(cast_failed(
479            from_type,
480            DataType::TimestampMicros,
481            CAST_REASON_INVALID_FORMAT,
482        ));
483    }
484
485    let (second, micros) = parse_second_micros(second_text, from_type)?;
486    if hour > 23 || minute > 59 || second > 59 {
487        return Err(cast_failed(
488            from_type,
489            DataType::TimestampMicros,
490            CAST_REASON_INVALID_FORMAT,
491        ));
492    }
493
494    let days = days_from_civil(year, month, day);
495    let seconds = days
496        .checked_mul(SECONDS_PER_DAY)
497        .and_then(|v| v.checked_add(i64::from(hour) * 3_600))
498        .and_then(|v| v.checked_add(i64::from(minute) * 60))
499        .and_then(|v| v.checked_add(i64::from(second)))
500        .ok_or_else(|| cast_failed(from_type, DataType::TimestampMicros, CAST_REASON_OVERFLOW))?;
501    seconds
502        .checked_mul(MICROS_PER_SECOND)
503        .and_then(|v| v.checked_add(i64::from(micros)))
504        .ok_or_else(|| cast_failed(from_type, DataType::TimestampMicros, CAST_REASON_OVERFLOW))
505}
506
507fn parse_date_part<T>(part: Option<&str>, from_type: DataType) -> Result<T>
508where
509    T: std::str::FromStr,
510{
511    part.and_then(|text| text.parse::<T>().ok()).ok_or_else(|| {
512        cast_failed(
513            from_type,
514            DataType::TimestampMicros,
515            CAST_REASON_INVALID_FORMAT,
516        )
517    })
518}
519
520fn parse_second_micros(input: &str, from_type: DataType) -> Result<(u32, u32)> {
521    let (second, fraction) = input.split_once('.').unwrap_or((input, ""));
522    let second = second.parse::<u32>().map_err(|_| {
523        cast_failed(
524            from_type,
525            DataType::TimestampMicros,
526            CAST_REASON_INVALID_FORMAT,
527        )
528    })?;
529    if fraction.is_empty() {
530        return Ok((second, 0));
531    }
532    if !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
533        return Err(cast_failed(
534            from_type,
535            DataType::TimestampMicros,
536            CAST_REASON_INVALID_FORMAT,
537        ));
538    }
539    if fraction.len() > 6 {
540        return Err(cast_failed(
541            from_type,
542            DataType::TimestampMicros,
543            CAST_REASON_PRECISION_LOSS,
544        ));
545    }
546
547    let mut micros = fraction.parse::<u32>().map_err(|_| {
548        cast_failed(
549            from_type,
550            DataType::TimestampMicros,
551            CAST_REASON_INVALID_FORMAT,
552        )
553    })?;
554    for _ in fraction.len()..6 {
555        micros *= 10;
556    }
557    Ok((second, micros))
558}
559
560fn format_timestamp_micros(micros: i64) -> String {
561    let seconds = micros.div_euclid(MICROS_PER_SECOND);
562    let micros_remainder = micros.rem_euclid(MICROS_PER_SECOND);
563    let days = seconds.div_euclid(SECONDS_PER_DAY);
564    let second_of_day = seconds.rem_euclid(SECONDS_PER_DAY);
565    let (year, month, day) = civil_from_days(days);
566    let hour = second_of_day / 3_600;
567    let minute = (second_of_day % 3_600) / 60;
568    let second = second_of_day % 60;
569
570    if micros_remainder == 0 {
571        format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
572    } else {
573        let mut fraction = format!("{micros_remainder:06}");
574        while fraction.ends_with('0') {
575            fraction.pop();
576        }
577        format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{fraction}Z")
578    }
579}
580
581fn valid_date(year: i32, month: u32, day: u32) -> bool {
582    (1..=12).contains(&month) && (1..=days_in_month(year, month)).contains(&day)
583}
584
585fn days_in_month(year: i32, month: u32) -> u32 {
586    match month {
587        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
588        4 | 6 | 9 | 11 => 30,
589        2 if is_leap_year(year) => 29,
590        2 => 28,
591        _ => 0,
592    }
593}
594
595fn is_leap_year(year: i32) -> bool {
596    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
597}
598
599fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
600    let year = i64::from(year) - i64::from(month <= 2);
601    let era = if year >= 0 { year } else { year - 399 } / 400;
602    let year_of_era = year - era * 400;
603    let shifted_month = i64::from(month) + if month > 2 { -3 } else { 9 };
604    let day_of_year = (153 * shifted_month + 2) / 5 + i64::from(day) - 1;
605    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
606    era * 146_097 + day_of_era - 719_468
607}
608
609fn civil_from_days(days: i64) -> (i64, i64, i64) {
610    let days = days + 719_468;
611    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
612    let day_of_era = days - era * 146_097;
613    let year_of_era =
614        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
615    let mut year = year_of_era + era * 400;
616    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
617    let month_prime = (5 * day_of_year + 2) / 153;
618    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
619    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
620    year += i64::from(month <= 2);
621    (year, month, day)
622}
623
624fn cast_failed(from_type: DataType, to_type: DataType, reason: &str) -> Error {
625    Error::CastFailed {
626        from_type: from_type.as_str().to_string(),
627        to_type: to_type.as_str().to_string(),
628        reason: reason.to_string(),
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::{can_cast, cast_value, DataType, DataValue};
635    use crate::Error;
636
637    fn assert_cast(input: DataValue, target: DataType, expected: DataValue) {
638        assert_eq!(cast_value(&input, target).unwrap(), expected);
639    }
640
641    fn assert_cast_error(input: DataValue, target: DataType, reason: &str) {
642        let err = cast_value(&input, target).unwrap_err();
643        match err {
644            Error::CastFailed {
645                from_type,
646                to_type,
647                reason: actual,
648            } => {
649                assert_eq!(from_type, input.data_type().as_str());
650                assert_eq!(to_type, target.as_str());
651                assert_eq!(actual, reason);
652            }
653            other => panic!("expected CastFailed, got {other:?}"),
654        }
655    }
656
657    #[test]
658    fn cast_matrix_marks_supported_major_paths() {
659        assert!(can_cast(DataType::Int32, DataType::Int64));
660        assert!(can_cast(DataType::Int32, DataType::Utf8));
661        assert!(can_cast(DataType::Utf8, DataType::Float64));
662        assert!(can_cast(DataType::Utf8, DataType::TimestampMicros));
663        assert!(can_cast(DataType::TimestampMicros, DataType::Int64));
664        assert!(can_cast(DataType::Float64, DataType::TimestampMicros));
665        assert!(can_cast(DataType::Null, DataType::TimestampMicros));
666    }
667
668    #[test]
669    fn numeric_casts_cover_widening_and_checked_narrowing() {
670        assert_cast(DataValue::Int32(42), DataType::Int64, DataValue::Int64(42));
671        assert_cast(
672            DataValue::Int64(16_777_216),
673            DataType::Float32,
674            DataValue::Float32(16_777_216.0),
675        );
676        assert_cast(
677            DataValue::Float64(42.0),
678            DataType::Int32,
679            DataValue::Int32(42),
680        );
681        assert_cast(
682            DataValue::Float32(1.25),
683            DataType::Float64,
684            DataValue::Float64(1.25),
685        );
686    }
687
688    #[test]
689    fn numeric_casts_report_stable_error_codes() {
690        assert_cast_error(
691            DataValue::Int64(i64::from(i32::MAX) + 1),
692            DataType::Int32,
693            "overflow",
694        );
695        assert_cast_error(DataValue::Float64(42.5), DataType::Int64, "precision_loss");
696        assert_cast_error(DataValue::Float64(f64::NAN), DataType::Int64, "non_finite");
697        assert_cast_error(
698            DataValue::Int64(16_777_217),
699            DataType::Float32,
700            "precision_loss",
701        );
702    }
703
704    #[test]
705    fn string_numeric_casts_parse_and_stringify() {
706        assert_cast(
707            DataValue::Utf8(" -12 ".into()),
708            DataType::Int32,
709            DataValue::Int32(-12),
710        );
711        assert_cast(
712            DataValue::Utf8("3.5".into()),
713            DataType::Float64,
714            DataValue::Float64(3.5),
715        );
716        assert_cast(
717            DataValue::Int64(99),
718            DataType::Utf8,
719            DataValue::Utf8("99".into()),
720        );
721        assert_cast_error(
722            DataValue::Utf8("nan".into()),
723            DataType::Int64,
724            "invalid_format",
725        );
726    }
727
728    #[test]
729    fn timestamp_string_and_epoch_second_casts_are_stable() {
730        assert_cast(
731            DataValue::Utf8("1970-01-01T00:00:01Z".into()),
732            DataType::TimestampMicros,
733            DataValue::TimestampMicros(1_000_000),
734        );
735        assert_cast(
736            DataValue::Utf8("1970-01-01 00:00:01.250000".into()),
737            DataType::TimestampMicros,
738            DataValue::TimestampMicros(1_250_000),
739        );
740        assert_cast(
741            DataValue::TimestampMicros(1_250_000),
742            DataType::Float64,
743            DataValue::Float64(1.25),
744        );
745        assert_cast(
746            DataValue::Float64(1.25),
747            DataType::TimestampMicros,
748            DataValue::TimestampMicros(1_250_000),
749        );
750        assert_cast(
751            DataValue::TimestampMicros(1_000_000),
752            DataType::Utf8,
753            DataValue::Utf8("1970-01-01T00:00:01Z".into()),
754        );
755        assert_cast_error(
756            DataValue::Utf8("1970-13-01T00:00:00Z".into()),
757            DataType::TimestampMicros,
758            "invalid_format",
759        );
760    }
761
762    #[test]
763    fn timestamp_micros_to_float64_rejects_precision_loss() {
764        assert_cast_error(
765            DataValue::TimestampMicros((1_i64 << 53) + 1),
766            DataType::Float64,
767            "precision_loss",
768        );
769    }
770
771    #[test]
772    fn null_casts_to_any_supported_type_without_changing_value() {
773        assert_cast(DataValue::Null, DataType::Int64, DataValue::Null);
774        assert_cast(DataValue::Null, DataType::Utf8, DataValue::Null);
775        assert_cast(DataValue::Null, DataType::TimestampMicros, DataValue::Null);
776    }
777}