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
21 changes: 1 addition & 20 deletions crates/krishiv-exec/src/planner/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ impl EvalEnv {
let zone = config
.get(&SESSION_TIME_ZONE)
.and_then(|v| v.as_str())
.and_then(fixed_offset);
.and_then(|z| krishiv_kernels::cast::FixedOffset::parse(z).map(|o| o.0));
let query_start_micros = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
Expand Down Expand Up @@ -211,25 +211,6 @@ impl EvalEnv {
}
}

/// The offset of `UTC`, `GMT`, `Z` or `[+-]hh:mm`, in seconds.
fn fixed_offset(zone: &str) -> Option<i32> {
if ["UTC", "GMT", "Z", "Etc/UTC"].contains(&zone) {
return Some(0);
}
let rest = zone
.strip_prefix("UTC")
.or_else(|| zone.strip_prefix("GMT"))
.unwrap_or(zone);
let (sign, hm) = match rest.as_bytes().first()? {
b'+' => (1, &rest[1..]),
b'-' => (-1, &rest[1..]),
_ => return None,
};
let (h, m) = hm.split_once(':').unwrap_or((hm, "0"));
let (h, m): (i32, i32) = (h.parse().ok()?, m.parse().ok()?);
((0..=18).contains(&h) && (0..60).contains(&m)).then(|| sign * (h * 3600 + m * 60))
}

/// The input an expression is compiled against.
#[derive(Clone, Copy)]
pub(crate) struct Input<'a> {
Expand Down
2 changes: 1 addition & 1 deletion crates/krishiv-kernels/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use arrow_buffer::{NullBuffer, ScalarBuffer, i256};
use arrow_schema::{DataType, TimeUnit};

pub use datetime::{FixedOffset, TimeZoneRules};
pub use timestamp::{TimestampText, timestamp_text};
pub use timestamp::{TimestampText, timestamp_has_zone, timestamp_text};

use crate::decimal::DecimalNative;
use crate::error::{KernelError, with_capacity};
Expand Down
72 changes: 72 additions & 0 deletions crates/krishiv-kernels/src/cast/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,42 @@ impl TimeZoneRules for FixedOffset {
}
}

impl FixedOffset {
/// The zone a `spark.sql.session.timeZone` value names, when it is a
/// fixed offset: `UTC`, `GMT`, `Z`, `Etc/UTC` or `[UTC|GMT][+-]hh[:mm]`
/// (hours up to 18). `None` for region ids, which need the time-zone
/// database, and for malformed zones.
#[must_use]
pub fn parse(zone: &str) -> Option<Self> {
let zone = zone.trim();
if ["UTC", "GMT", "Z", "Etc/UTC"].contains(&zone) {
return Some(Self(0));
}
let rest = zone
.strip_prefix("UTC")
.or_else(|| zone.strip_prefix("GMT"))
.unwrap_or(zone);
let (sign, hm) = match rest.as_bytes().first()? {
b'+' => (1, &rest[1..]),
b'-' => (-1, &rest[1..]),
_ => return None,
};
let (h_str, m_str) = hm.split_once(':').unwrap_or((hm, "0"));
// `i32::from_str` accepts a leading `+`/`-`, so `++5` or
// `+05:+30` would parse; require 1-2 ASCII digits per field.
if !(1..=2).contains(&h_str.len()) || !(1..=2).contains(&m_str.len()) {
return None;
}
if !h_str.bytes().all(|b| b.is_ascii_digit()) || !m_str.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let (h, m): (i32, i32) = (h_str.parse().ok()?, m_str.parse().ok()?);
let total = h * 3600 + m * 60;
(h <= 18 && m < 60 && total <= 18 * 3600).then(|| Self(sign * total))
}
}

/// Days since 1970-01-01 of a proleptic Gregorian date, or `None` for an
/// invalid month/day (Howard Hinnant's `days_from_civil`).
pub(crate) fn days_from_civil(year: i64, month: u32, day: u32) -> Option<i64> {
Expand Down Expand Up @@ -143,6 +179,42 @@ pub(crate) fn utc_to_local(utc: i64, tz: &dyn TimeZoneRules) -> Option<i64> {
mod tests {
use super::*;

#[test]
fn session_zones_parse_as_fixed_offsets_or_none() {
for (zone, offset) in [
("UTC", Some(0)),
(" GMT ", Some(0)),
("Z", Some(0)),
("Etc/UTC", Some(0)),
("+05:30", Some(19_800)),
("UTC-08:00", Some(-28_800)),
("GMT+1", Some(3600)),
("-18:00", Some(-64_800)),
("+18:00", Some(64_800)),
] {
assert_eq!(FixedOffset::parse(zone), offset.map(FixedOffset), "{zone}");
}
// Region ids need the tz database; malformed or out-of-range
// offsets are not zones.
for zone in [
"America/Los_Angeles",
"",
"+19:00",
"+05:60",
"+ab",
"UTC*1",
"++5",
"+05:+30",
"+18:30",
"+18:01",
"-18:30",
"+5:30:00",
"+",
] {
assert_eq!(FixedOffset::parse(zone), None, "{zone}");
}
}

#[test]
fn civil_round_trips_and_spark_values() {
// Values recorded from Spark 4.2 (`unix_date`).
Expand Down
11 changes: 11 additions & 0 deletions crates/krishiv-kernels/src/cast/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ pub fn timestamp_text(s: &str) -> TimestampText {
}
}

/// Whether `s` names an explicit zone (an offset or region id after the
/// seconds), as the cast kernel's parse sees it. Session-independent
/// timestamp strings evaluate the same in every session zone.
#[must_use]
pub fn timestamp_has_zone(s: &str) -> bool {
matches!(
parse(s.as_bytes()),
Parsed::At(_, Zone::Offset(_) | Zone::Region)
)
}

/// Parses one string.
pub(crate) fn parse(s: &[u8]) -> Parsed {
let s = trim_all(s);
Expand Down
Loading
Loading