use alloc::format;
use alloc::string::String;
use spg_storage::Value;
use super::{EvalError, MONTH_ABBR, MONTH_FULL, civil_from_days, days_from_civil};
#[inline(never)]
pub(super) fn mysql_compound_extract(name: &str, v: &Value) -> Option<Value<'static>> {
let (days, tod_us): (i32, i64) = match v {
Value::Date(d) => (*d, 0),
Value::Timestamp(us) => (
i32::try_from(us.div_euclid(86_400_000_000)).ok()?,
us.rem_euclid(86_400_000_000),
),
_ => return None,
};
let (y, mo, d) = civil_from_days(days);
let (y, mo, d) = (i64::from(y), i64::from(mo), i64::from(d));
let hh = tod_us / 3_600_000_000;
let mi = (tod_us / 60_000_000) % 60;
let ss = (tod_us / 1_000_000) % 60;
let us = tod_us % 1_000_000;
let n: i64 = match name {
"year_month" => y * 100 + mo,
"day_hour" => d * 100 + hh,
"day_minute" => d * 10_000 + hh * 100 + mi,
"day_second" => d * 1_000_000 + hh * 10_000 + mi * 100 + ss,
"hour_minute" => hh * 100 + mi,
"hour_second" => hh * 10_000 + mi * 100 + ss,
"minute_second" => mi * 100 + ss,
"day_microsecond" => {
d * 1_000_000_000_000 + hh * 10_000_000_000 + mi * 100_000_000 + ss * 1_000_000 + us
}
"hour_microsecond" => hh * 10_000_000_000 + mi * 100_000_000 + ss * 1_000_000 + us,
"minute_microsecond" => mi * 100_000_000 + ss * 1_000_000 + us,
"second_microsecond" => ss * 1_000_000 + us,
_ => return None,
};
Some(Value::Numeric {
scaled: i128::from(n),
scale: 0,
kind: spg_storage::NumericKind::Finite,
})
}
pub(super) fn extract_field(
field: &spg_sql::ast::ExtractField,
v: &Value,
src_name: &str,
) -> Result<Value<'static>, EvalError> {
use spg_sql::ast::ExtractField as F;
if matches!(v, Value::Null) {
return Ok(Value::Null);
}
if let F::Other(name) = field {
return Err(EvalError::TypeMismatch {
detail: format!("unit \"{name}\" not recognized for type {src_name}"),
});
}
let num0 = |n: i64| Value::Numeric {
scaled: i128::from(n),
scale: 0,
kind: spg_storage::NumericKind::Finite,
};
if let Value::TimeTz { us, offset_secs } = *v {
let secs = us / 1_000_000;
let frac = us % 1_000_000;
let num = |scaled: i128, scale: u16| {
Ok(Value::Numeric {
scaled,
scale,
kind: spg_storage::NumericKind::Finite,
})
};
return match field {
F::Hour => num(i128::from(secs / 3600), 0),
F::Minute => num(i128::from((secs / 60) % 60), 0),
F::Second => num(i128::from(secs % 60) * 1_000_000 + i128::from(frac), 6),
F::Millisecond => num(i128::from(secs % 60) * 1_000_000 + i128::from(frac), 3),
F::Microsecond => num(i128::from(secs % 60) * 1_000_000 + i128::from(frac), 0),
F::Epoch => num(i128::from(us) - i128::from(offset_secs) * 1_000_000, 6),
F::Timezone => num(i128::from(offset_secs), 0),
F::TimezoneHour => num(i128::from(offset_secs / 3600), 0),
F::TimezoneMinute => num(i128::from((offset_secs / 60) % 60), 0),
other => Err(EvalError::TypeMismatch {
detail: format!(
"unit \"{}\" not supported for type {src_name}",
format!("{other}").to_lowercase()
),
}),
};
}
if let Value::Interval {
months,
days,
micros,
} = *v
{
let years = months / 12;
let mons = months % 12;
let secs_total = micros / 1_000_000;
let frac = micros % 1_000_000;
match field {
F::Epoch => {
let total_secs =
i64::from(months) * 30 * 86_400 + i64::from(days) * 86_400 + secs_total;
return Ok(Value::Numeric {
scaled: i128::from(total_secs) * 1_000_000 + i128::from(frac),
scale: 6,
kind: spg_storage::NumericKind::Finite,
});
}
F::Second => {
return Ok(Value::Numeric {
scaled: i128::from(secs_total % 60) * 1_000_000 + i128::from(frac),
scale: 6,
kind: spg_storage::NumericKind::Finite,
});
}
F::Millisecond => {
return Ok(Value::Numeric {
scaled: i128::from(secs_total % 60) * 1_000_000 + i128::from(frac),
scale: 3,
kind: spg_storage::NumericKind::Finite,
});
}
_ => {}
}
let result = match field {
F::Year => i64::from(years),
F::Month => i64::from(mons),
F::Day => i64::from(days),
F::Hour => secs_total / 3600,
F::Minute => (secs_total / 60) % 60,
F::Second => secs_total % 60,
F::Microsecond => (secs_total % 60) * 1_000_000 + frac,
F::Epoch => i64::from(months) * 30 * 86_400 + i64::from(days) * 86_400 + secs_total,
F::Quarter => i64::from(mons) / 3 + 1,
F::Decade => i64::from(years) / 10,
F::Century => i64::from(years) / 100,
F::Millennium => i64::from(years) / 1000,
F::Millisecond => (secs_total % 60) * 1_000 + frac / 1_000,
F::Week => i64::from(days) / 7,
F::Dow
| F::Isodow
| F::Doy
| F::Isoyear
| F::Julian
| F::Timezone
| F::TimezoneHour
| F::TimezoneMinute => {
return Err(EvalError::TypeMismatch {
detail: format!(
"unit \"{}\" not supported for type {src_name}",
format!("{field}").to_lowercase()
),
});
}
F::Other(_) => unreachable!("handled above"),
};
return Ok(num0(result));
}
if let Value::Time(micros) = *v {
let secs = micros / 1_000_000;
let frac = micros % 1_000_000;
let num = |scaled: i128, scale: u16| {
Ok(Value::Numeric {
scaled,
scale,
kind: spg_storage::NumericKind::Finite,
})
};
return match field {
F::Hour => num(i128::from(secs / 3600), 0),
F::Minute => num(i128::from((secs / 60) % 60), 0),
F::Second => num(i128::from(secs % 60) * 1_000_000 + i128::from(frac), 6),
F::Millisecond => num(i128::from(secs % 60) * 1_000_000 + i128::from(frac), 3),
F::Microsecond => num(i128::from(secs % 60) * 1_000_000 + i128::from(frac), 0),
F::Epoch => num(i128::from(secs) * 1_000_000 + i128::from(frac), 6),
other => Err(EvalError::TypeMismatch {
detail: format!(
"unit \"{}\" not supported for type {src_name}",
alloc::format!("{other}").to_lowercase()
),
}),
};
}
if matches!(*v, Value::Date(_))
&& matches!(
field,
F::Hour
| F::Minute
| F::Second
| F::Millisecond
| F::Microsecond
| F::Timezone
| F::TimezoneHour
| F::TimezoneMinute
)
{
return Err(EvalError::TypeMismatch {
detail: format!(
"unit \"{}\" not supported for type {src_name}",
format!("{field}").to_lowercase()
),
});
}
let (days, day_micros) = match *v {
Value::Date(d) => (d, 0_i64),
Value::Timestamp(t) => {
let days = t.div_euclid(86_400_000_000);
let day_micros = t.rem_euclid(86_400_000_000);
(i32::try_from(days).unwrap_or(i32::MAX), day_micros)
}
_ => {
return Err(EvalError::TypeMismatch {
detail: format!(
"EXTRACT requires DATE / TIMESTAMP / INTERVAL, got {}",
crate::conversions::pg_type_name_for_error_opt(v.data_type())
),
});
}
};
let (y, m, d) = civil_components(days);
let secs = day_micros / 1_000_000;
let hh = secs / 3600;
let mm = (secs / 60) % 60;
let ss = secs % 60;
let frac = day_micros % 1_000_000;
match field {
F::Epoch => {
let total_secs = i64::from(days) * 86_400 + secs;
if matches!(*v, Value::Date(_)) {
return Ok(Value::Numeric {
scaled: i128::from(total_secs),
scale: 0,
kind: spg_storage::NumericKind::Finite,
});
}
return Ok(Value::Numeric {
scaled: i128::from(total_secs) * 1_000_000 + i128::from(frac),
scale: 6,
kind: spg_storage::NumericKind::Finite,
});
}
F::Second => {
return Ok(Value::Numeric {
scaled: i128::from(ss) * 1_000_000 + i128::from(frac),
scale: 6,
kind: spg_storage::NumericKind::Finite,
});
}
F::Millisecond => {
return Ok(Value::Numeric {
scaled: i128::from(ss) * 1_000_000 + i128::from(frac),
scale: 3,
kind: spg_storage::NumericKind::Finite,
});
}
_ => {}
}
let result = match field {
F::Year => {
if y <= 0 {
i64::from(y) - 1
} else {
i64::from(y)
}
}
F::Month => i64::from(m),
F::Day => i64::from(d),
F::Hour => hh,
F::Minute => mm,
F::Second => ss,
F::Microsecond => ss * 1_000_000 + frac,
F::Epoch => i64::from(days) * 86_400 + secs,
F::Dow => i64::from((days + 4).rem_euclid(7)),
F::Isodow => i64::from((days + 3).rem_euclid(7)) + 1,
F::Doy => i64::from(days - days_from_civil(y, 1, 1)) + 1,
F::Week => iso_week_and_year(days, y).0,
F::Isoyear => {
let iso = iso_week_and_year(days, y).1;
if iso <= 0 { iso - 1 } else { iso }
}
F::Quarter => i64::from((m - 1) / 3) + 1,
F::Decade => i64::from(y).div_euclid(10),
F::Century => era_bucket(y, 100),
F::Millennium => era_bucket(y, 1000),
F::Julian => {
if matches!(*v, Value::Timestamp(_)) {
let jd = i128::from(days) + 2_440_588;
let scaled = jd * 10_i128.pow(20)
+ i128::from(day_micros) * 10_i128.pow(20) / 86_400_000_000;
return Ok(Value::Numeric {
scaled,
scale: 20,
kind: spg_storage::NumericKind::Finite,
});
}
i64::from(days) + 2_440_588
}
F::Millisecond => ss * 1_000 + frac / 1_000,
F::Timezone | F::TimezoneHour | F::TimezoneMinute => 0,
F::Other(_) => unreachable!("handled above"),
};
Ok(num0(result))
}
pub(super) fn value_src_type_name(v: &Value) -> &'static str {
match v {
Value::Date(_) => "date",
Value::Time(_) => "time without time zone",
Value::TimeTz { .. } => "time with time zone",
Value::Interval { .. } => "interval",
_ => "timestamp without time zone",
}
}
fn era_bucket(y: i32, unit: i32) -> i64 {
if y > 0 {
i64::from((y - 1) / unit) + 1
} else {
i64::from(y / unit) - 1
}
}
pub(crate) fn iso_week_and_year(days: i32, y: i32) -> (i64, i64) {
let isodow = (days + 3).rem_euclid(7) + 1; let doy = days - days_from_civil(y, 1, 1) + 1;
let iso_weeks_in = |year: i32| -> i32 {
let jan1 = days_from_civil(year, 1, 1);
let dec31 = days_from_civil(year, 12, 31);
let jan1_isodow = (jan1 + 3).rem_euclid(7) + 1;
let dec31_isodow = (dec31 + 3).rem_euclid(7) + 1;
if jan1_isodow == 4 || dec31_isodow == 4 {
53
} else {
52
}
};
let w = (doy - isodow + 10).div_euclid(7);
if w < 1 {
(i64::from(iso_weeks_in(y - 1)), i64::from(y - 1))
} else if w > iso_weeks_in(y) {
(1, i64::from(y + 1))
} else {
(i64::from(w), i64::from(y))
}
}
fn civil_components(days: i32) -> (i32, u32, u32) {
civil_from_days(days)
}
pub(super) fn date_part(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
use spg_sql::ast::ExtractField as F;
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("date_part() takes 2 args, got {}", args.len()),
});
}
if matches!(&args[0], Value::Null) || matches!(&args[1], Value::Null) {
return Ok(Value::Null);
}
let Value::Text(field_name) = &args[0] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"date_part() needs a text field, got {}",
crate::conversions::pg_type_name_for_error_opt(args[0].data_type())
),
});
};
let field = match field_name.to_ascii_lowercase().as_str() {
"year" | "years" => F::Year,
"month" | "months" => F::Month,
"day" | "days" => F::Day,
"hour" | "hours" => F::Hour,
"minute" | "minutes" => F::Minute,
"second" | "seconds" => F::Second,
"microsecond" | "microseconds" => F::Microsecond,
"epoch" => F::Epoch,
"dow" => F::Dow,
"isodow" => F::Isodow,
"doy" => F::Doy,
"week" | "weeks" => F::Week,
"isoyear" => F::Isoyear,
"quarter" => F::Quarter,
"decade" | "decades" => F::Decade,
"century" | "centuries" => F::Century,
"millennium" | "millenniums" | "millennia" => F::Millennium,
"julian" => F::Julian,
"millisecond" | "milliseconds" => F::Millisecond,
"timezone" => F::Timezone,
"timezone_hour" => F::TimezoneHour,
"timezone_minute" => F::TimezoneMinute,
other => F::Other(String::from(other)),
};
let promoted;
let mut from_date = false;
let (arg, src_name) = match &args[1] {
Value::Date(d) => {
promoted = Value::Timestamp(i64::from(*d) * 86_400_000_000);
from_date = true;
(&promoted, "timestamp without time zone")
}
other => (other, value_src_type_name(other)),
};
if matches!(field, F::Timezone | F::TimezoneHour | F::TimezoneMinute) && from_date {
return Err(EvalError::TypeMismatch {
detail: format!(
"unit \"{}\" not supported for type timestamp without time zone",
format!("{field}").to_lowercase()
),
});
}
Ok(match extract_field(&field, arg, src_name)? {
Value::Numeric { scaled, scale, .. } =>
{
#[allow(clippy::cast_precision_loss)]
Value::Float(scaled as f64 / 10f64.powi(i32::from(scale)))
}
other => other,
})
}
pub(super) fn age(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.is_empty() || args.len() > 2 {
return Err(EvalError::TypeMismatch {
detail: format!("age() takes 1 or 2 args, got {}", args.len()),
});
}
if args.iter().any(|v| matches!(v, Value::Null)) {
return Ok(Value::Null);
}
if args.len() == 1 {
let n = match &args[0] {
Value::SmallInt(_) => Some("smallint"),
Value::Int(_) => Some("integer"),
Value::BigInt(_) => Some("bigint"),
_ => None,
};
if let Some(n) = n {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("function age({n}) does not exist"),
});
}
}
let to_micros = |v: &Value| -> Result<i64, EvalError> {
match v {
Value::Timestamp(t) => Ok(*t),
Value::Date(d) => Ok(i64::from(*d) * 86_400_000_000),
other => Err(EvalError::TypeMismatch {
detail: format!(
"age() needs DATE or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
};
let (a, b) = if args.len() == 1 {
const ANCHOR_2020_UTC: i64 = 1_577_836_800_000_000;
(ANCHOR_2020_UTC, to_micros(&args[0])?)
} else {
(to_micros(&args[0])?, to_micros(&args[1])?)
};
const US_PER_DAY: i64 = 86_400_000_000;
let neg = a < b;
let (hi, lo) = if neg { (b, a) } else { (a, b) };
let split = |us: i64| -> (i32, i64) {
(
i32::try_from(us.div_euclid(US_PER_DAY)).unwrap_or(i32::MAX),
us.rem_euclid(US_PER_DAY),
)
};
let (hd, ht) = split(hi);
let (ld, lt) = split(lo);
let (y1, m1, d1) = civil_from_days(hd);
let (y2, m2, d2) = civil_from_days(ld);
let dim = |y: i32, m: u32| -> i64 {
let (ny, nm) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
i64::from(days_from_civil(ny, nm, 1) - days_from_civil(y, m, 1))
};
let mut micros = ht - lt;
let mut mday = i64::from(d1) - i64::from(d2);
let mut mon = i64::from(m1) - i64::from(m2);
let mut year = i64::from(y1) - i64::from(y2);
if micros < 0 {
micros += US_PER_DAY;
mday -= 1;
}
while mday < 0 {
mon -= 1;
mday += dim(y2, m2);
}
while mon < 0 {
mon += 12;
year -= 1;
}
let mut months = year * 12 + mon;
if neg {
months = -months;
mday = -mday;
micros = -micros;
}
Ok(Value::Interval {
months: i32::try_from(months).map_err(|_| EvalError::TypeMismatch {
detail: "age() month count exceeds i32".into(),
})?,
days: i32::try_from(mday).map_err(|_| EvalError::TypeMismatch {
detail: "age() day count exceeds i32".into(),
})?,
micros,
})
}
pub(super) fn date_format_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
use core::fmt::Write as _;
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("date_format() takes 2 args, got {}", args.len()),
});
}
if matches!(&args[0], Value::Null) || matches!(&args[1], Value::Null) {
return Ok(Value::Null);
}
let Value::Text(fmt) = &args[1] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"date_format() needs a text format, got {}",
crate::conversions::pg_type_name_for_error_opt(args[1].data_type())
),
});
};
let (days, day_micros) = match &args[0] {
Value::Date(d) => (*d, 0_i64),
Value::Timestamp(t) => {
let days = t.div_euclid(86_400_000_000);
(
i32::try_from(days).unwrap_or(i32::MAX),
t.rem_euclid(86_400_000_000),
)
}
Value::Text(s) => {
let t = parse_text_datetime(s).ok_or_else(|| EvalError::TypeMismatch {
detail: format!("date_format(): cannot parse datetime {s:?}"),
})?;
let days = t.div_euclid(86_400_000_000);
(
i32::try_from(days).unwrap_or(i32::MAX),
t.rem_euclid(86_400_000_000),
)
}
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"date_format() needs DATE or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let (y, mo, d) = civil_from_days(days);
let secs = day_micros / 1_000_000;
let frac = day_micros % 1_000_000;
let hh24 = u32::try_from(secs / 3600).unwrap_or(0);
let mi = u32::try_from((secs / 60) % 60).unwrap_or(0);
let ss = u32::try_from(secs % 60).unwrap_or(0);
let hh12 = match hh24 % 12 {
0 => 12,
x => x,
};
let ampm = if hh24 < 12 { "AM" } else { "PM" };
let us = u32::try_from(frac).unwrap_or(0);
let mut out = String::with_capacity(fmt.len() + 8);
let bytes = fmt.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'%' {
out.push(bytes[i] as char);
i += 1;
continue;
}
if i + 1 >= bytes.len() {
out.push('%');
i += 1;
continue;
}
let token = bytes[i + 1];
match token {
b'Y' => {
let _ = write!(out, "{y:04}");
}
b'y' => {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let yy = (y.rem_euclid(100)) as u32;
let _ = write!(out, "{yy:02}");
}
b'm' => {
let _ = write!(out, "{mo:02}");
}
b'c' => {
let _ = write!(out, "{mo}");
}
b'd' => {
let _ = write!(out, "{d:02}");
}
b'e' => {
let _ = write!(out, "{d}");
}
b'H' => {
let _ = write!(out, "{hh24:02}");
}
b'h' | b'I' => {
let _ = write!(out, "{hh12:02}");
}
b'i' => {
let _ = write!(out, "{mi:02}");
}
b's' | b'S' => {
let _ = write!(out, "{ss:02}");
}
b'f' => {
let _ = write!(out, "{us:06}");
}
b'p' => {
out.push_str(ampm);
}
b'M' => {
out.push_str(MONTH_FULL[(mo - 1) as usize]);
}
b'b' => {
out.push_str(MONTH_ABBR[(mo - 1) as usize]);
}
b'%' => {
out.push('%');
}
b'k' => {
let _ = write!(out, "{hh24}");
}
b'l' => {
let _ = write!(out, "{hh12}");
}
b'W' => {
out.push_str(DAY_FULL[weekday_sunday0(days) as usize]);
}
b'a' => {
out.push_str(DAY_ABBR[weekday_sunday0(days) as usize]);
}
b'w' => {
let _ = write!(out, "{}", weekday_sunday0(days));
}
b'j' => {
let _ = write!(out, "{:03}", day_of_year(y, mo, d));
}
b'D' => {
let _ = write!(out, "{d}{}", ordinal_suffix(d));
}
b'r' => {
let _ = write!(out, "{hh12:02}:{mi:02}:{ss:02} {ampm}");
}
b'T' => {
let _ = write!(out, "{hh24:02}:{mi:02}:{ss:02}");
}
b'U' => {
let _ = write!(out, "{:02}", week_of_year(y, mo, d, days, false));
}
b'u' => {
let _ = write!(out, "{:02}", week_of_year(y, mo, d, days, true));
}
b'V' => {
let _ = write!(out, "{:02}", sunday_week(days).1);
}
b'X' => {
let _ = write!(out, "{:04}", sunday_week(days).0);
}
b'v' => {
let _ = write!(out, "{:02}", iso_week(days).1);
}
b'x' => {
let _ = write!(out, "{:04}", iso_week(days).0);
}
b'Z' => {
out.push_str("UTC");
}
other => {
out.push(other as char);
}
}
i += 2;
}
Ok(Value::text(out))
}
const DAY_FULL: [&str; 7] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const DAY_ABBR: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
fn weekday_sunday0(days: i32) -> u32 {
u32::try_from((days + 4).rem_euclid(7)).unwrap_or(0)
}
fn ordinal_suffix(d: u32) -> &'static str {
match (d % 10, d % 100) {
(1, 1 | 21 | 31) => "st",
(2, 2 | 22) => "nd",
(3, 3 | 23) => "rd",
_ => "th",
}
}
fn day_of_year(y: i32, mo: u32, d: u32) -> u32 {
const CUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
CUM[(mo - 1) as usize] + d + u32::from(leap && mo > 2)
}
pub(crate) fn mysql_calc_week(days: i32, mode: u32) -> (i32, u32) {
let mode = {
let m = mode & 7;
if m & 1 == 0 { m ^ 4 } else { m }
};
let monday_first = mode & 1 != 0;
let mut week_year = mode & 2 != 0;
let first_weekday = mode & 4 != 0;
let weekday = |dn: i32| -> i32 {
if monday_first {
(dn + 3).rem_euclid(7)
} else {
(dn + 4).rem_euclid(7)
}
};
let days_in_year = |yr: i32| -> i32 {
if (yr % 4 == 0 && yr % 100 != 0) || yr % 400 == 0 {
366
} else {
365
}
};
let partial_before = |wd: i32| -> bool { if first_weekday { wd != 0 } else { wd >= 4 } };
let (mut year, mo, d) = civil_from_days(days);
let mut first_daynr = days_from_civil(year, 1, 1);
let mut wd = weekday(first_daynr);
if mo == 1 && i32::try_from(d).unwrap_or(1) <= 7 - wd {
if !week_year && partial_before(wd) {
return (year, 0);
}
week_year = true;
year -= 1;
let diy = days_in_year(year);
first_daynr -= diy;
wd = (wd + 7 - diy.rem_euclid(7)).rem_euclid(7);
}
let offset = if partial_before(wd) {
days - (first_daynr + (7 - wd))
} else {
days - (first_daynr - wd)
};
if week_year && offset >= 52 * 7 {
let diy = days_in_year(year);
let wd_end = (wd + diy).rem_euclid(7);
if !partial_before(wd_end) {
return (year + 1, 1);
}
}
(year, u32::try_from(offset / 7 + 1).unwrap_or(1))
}
fn week_of_year(y: i32, mo: u32, d: u32, days: i32, monday_first: bool) -> u32 {
let doy = day_of_year(y, mo, d);
let w = if monday_first {
(weekday_sunday0(days) + 6) % 7
} else {
weekday_sunday0(days)
};
(doy + 6 - w) / 7
}
fn sunday_week(days: i32) -> (i32, u32) {
reckon_week(days, 0)
}
fn iso_week(days: i32) -> (i32, u32) {
reckon_week(days, 1)
}
fn reckon_week(days: i32, start: u32) -> (i32, u32) {
let (y, _, _) = civil_from_days(days);
for probe in [y + 1, y, y - 1] {
let first = days_from_civil(probe, 1, 1);
let first_wd = weekday_sunday0(first);
let offset = i32::try_from((start + 7 - first_wd) % 7).unwrap_or(0);
let week1 = if start == 1 {
let jan4 = days_from_civil(probe, 1, 4);
jan4 - i32::try_from((weekday_sunday0(jan4) + 6) % 7).unwrap_or(0)
} else {
first + offset
};
if days >= week1 {
let w = u32::try_from((days - week1) / 7 + 1).unwrap_or(1);
return (probe, w);
}
}
(y, 1)
}
pub(super) fn unix_timestamp_of(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 1 {
return Err(EvalError::TypeMismatch {
detail: format!("unix_timestamp() takes 0 or 1 arg, got {}", args.len()),
});
}
match &args[0] {
Value::Null => Ok(Value::Null),
Value::Timestamp(t) => Ok(unix_seconds(*t)),
Value::Date(d) => Ok(Value::BigInt(i64::from(*d) * 86_400)),
Value::Text(t) | Value::BpChar(t) => Ok(text_unix_seconds(t)),
Value::Int(_) | Value::BigInt(_) | Value::SmallInt(_) => {
let n = match &args[0] {
Value::Int(n) => i64::from(*n),
Value::SmallInt(n) => i64::from(*n),
Value::BigInt(n) => *n,
_ => unreachable!("guarded above"),
};
Ok(text_unix_seconds(&alloc::format!("{n}")))
}
other => Err(EvalError::TypeMismatch {
detail: format!(
"unix_timestamp() needs DATE or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn unix_seconds(micros: i64) -> Value<'static> {
let frac = micros.rem_euclid(1_000_000);
if frac == 0 {
Value::BigInt(micros.div_euclid(1_000_000))
} else {
#[allow(clippy::cast_precision_loss)]
Value::Float(micros as f64 / 1_000_000.0)
}
}
fn text_unix_seconds(t: &str) -> Value<'static> {
let trimmed = t.trim();
if trimmed.len() == 8 && trimmed.bytes().all(|b| b.is_ascii_digit()) {
let iso = alloc::format!("{}-{}-{}", &trimmed[..4], &trimmed[4..6], &trimmed[6..]);
return crate::eval::parse_date_literal(&iso)
.map_or(Value::Null, |d| Value::BigInt(i64::from(d) * 86_400));
}
if let Some(micros) = crate::eval::parse_timestamp_literal(trimmed) {
return unix_seconds(micros);
}
crate::eval::parse_date_literal(trimmed)
.map_or(Value::Null, |d| Value::BigInt(i64::from(d) * 86_400))
}
pub(super) fn from_unixtime(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if !(1..=2).contains(&args.len()) {
return Err(EvalError::TypeMismatch {
detail: format!("from_unixtime() takes 1 or 2 args, got {}", args.len()),
});
}
if args.iter().any(|v| matches!(v, Value::Null)) {
return Ok(Value::Null);
}
let secs: i64 = match &args[0] {
Value::SmallInt(n) => i64::from(*n),
Value::Int(n) => i64::from(*n),
Value::BigInt(n) => *n,
Value::Float(x) => *x as i64,
Value::Numeric { scaled, scale, .. } => {
let denom = 10_i128.pow(u32::from(*scale));
i64::try_from(scaled.div_euclid(denom)).unwrap_or(i64::MAX)
}
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"from_unixtime() needs a numeric epoch second count, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let ts = Value::Timestamp(secs.saturating_mul(1_000_000));
if args.len() == 1 {
Ok(ts)
} else {
date_format_mysql(&[ts, args[1].clone()])
}
}
pub(super) fn date_trunc(
args: &[Value<'_>],
ctx: &super::EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
if args.len() == 3 {
if args.iter().any(|a| matches!(a, Value::Null)) {
return Ok(Value::Null);
}
let Value::Text(zone) = &args[2] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"date_trunc() zone must be text, got {}",
crate::conversions::pg_type_name_for_error_opt(args[2].data_type())
),
});
};
let z = zone.trim();
let ts = text_or_temporal_micros(&args[1], "date_trunc")?;
let off = ctx
.zone_offset_at(z, ts)
.ok_or_else(|| EvalError::TypeMismatch {
detail: format!("date_trunc({z:?}): time zone not recognized"),
})?;
let local = Value::Timestamp(ts + off);
let truncated = date_trunc(&[args[0].clone(), local], ctx)?;
let Value::Timestamp(tl) = truncated else {
return Ok(truncated);
};
let utc = ctx.zone_local_to_utc(z, tl).unwrap_or(tl - off);
return Ok(Value::Timestamp(utc));
}
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("date_trunc() takes 2 args, got {}", args.len()),
});
}
if matches!(&args[0], Value::Null) || matches!(&args[1], Value::Null) {
return Ok(Value::Null);
}
let Value::Text(unit) = &args[0] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"date_trunc() needs a text unit, got {}",
crate::conversions::pg_type_name_for_error_opt(args[0].data_type())
),
});
};
if let Value::Interval {
months,
days,
micros,
} = &args[1]
{
let unit_lc = unit.to_ascii_lowercase();
let (mut mo, mut dd, mut us) = (*months, *days, *micros);
match unit_lc.as_str() {
"millennium" => {
mo = mo / 12000 * 12000;
dd = 0;
us = 0;
}
"century" => {
mo = mo / 1200 * 1200;
dd = 0;
us = 0;
}
"decade" => {
mo = mo / 120 * 120;
dd = 0;
us = 0;
}
"year" => {
mo = mo / 12 * 12;
dd = 0;
us = 0;
}
"quarter" => {
mo = mo / 3 * 3;
dd = 0;
us = 0;
}
"month" => {
dd = 0;
us = 0;
}
"day" => us = 0,
"hour" => us = us / 3_600_000_000 * 3_600_000_000,
"minute" => us = us / 60_000_000 * 60_000_000,
"second" => us = us / 1_000_000 * 1_000_000,
"milliseconds" | "millisecond" => us = us / 1_000 * 1_000,
"microseconds" | "microsecond" => {}
other => {
return Err(EvalError::TypeMismatch {
detail: format!("unit \"{other}\" not supported for type interval"),
});
}
}
return Ok(Value::Interval {
months: mo,
days: dd,
micros: us,
});
}
let micros = match &args[1] {
Value::Timestamp(t) => *t,
Value::Date(d) => i64::from(*d) * 86_400_000_000,
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"date_trunc() needs DATE or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let unit_lc = unit.to_ascii_lowercase();
let days = micros.div_euclid(86_400_000_000);
let day_micros = micros.rem_euclid(86_400_000_000);
let day_i32 = i32::try_from(days).unwrap_or(i32::MAX);
let (y, m, _) = civil_from_days(day_i32);
const DAY: i64 = 86_400_000_000;
let truncated = match unit_lc.as_str() {
"millennium" => {
let my = if y > 0 {
(y - 1) / 1000 * 1000 + 1
} else {
y / 1000 * 1000 - 999
};
i64::from(days_from_civil(my, 1, 1)) * DAY
}
"century" => {
let cy = if y > 0 {
(y - 1) / 100 * 100 + 1
} else {
y / 100 * 100 - 99
};
i64::from(days_from_civil(cy, 1, 1)) * DAY
}
"decade" => i64::from(days_from_civil(y.div_euclid(10) * 10, 1, 1)) * DAY,
"year" => i64::from(days_from_civil(y, 1, 1)) * DAY,
"quarter" => {
let qm = (m - 1) / 3 * 3 + 1;
i64::from(days_from_civil(y, qm, 1)) * DAY
}
"month" => i64::from(days_from_civil(y, m, 1)) * DAY,
"week" => {
let isodow = (day_i32 + 3).rem_euclid(7); i64::from(day_i32 - isodow) * DAY
}
"day" => days * DAY,
"hour" => days * DAY + (day_micros / 3_600_000_000) * 3_600_000_000,
"minute" => days * DAY + (day_micros / 60_000_000) * 60_000_000,
"second" => days * DAY + (day_micros / 1_000_000) * 1_000_000,
"milliseconds" | "millisecond" => days * DAY + (day_micros / 1_000) * 1_000,
"microseconds" | "microsecond" => micros,
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"unknown date_trunc unit {other:?}; supported: millennium, \
century, decade, year, quarter, month, week, day, hour, \
minute, second, milliseconds, microseconds"
),
});
}
};
Ok(Value::Timestamp(truncated))
}
pub(super) fn str_to_date_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("str_to_date() takes 2 args, got {}", args.len()),
});
}
if matches!(&args[0], Value::Null) || matches!(&args[1], Value::Null) {
return Ok(Value::Null);
}
let (Value::Text(input), Value::Text(fmt)) = (&args[0], &args[1]) else {
return Err(EvalError::TypeMismatch {
detail: format!(
"str_to_date() takes (text, text), got ({:?}, {:?})",
args[0].data_type(),
args[1].data_type()
),
});
};
const MONTH_FULL_UP: [&str; 12] = [
"JANUARY",
"FEBRUARY",
"MARCH",
"APRIL",
"MAY",
"JUNE",
"JULY",
"AUGUST",
"SEPTEMBER",
"OCTOBER",
"NOVEMBER",
"DECEMBER",
];
let inp: alloc::vec::Vec<char> = input.chars().collect();
let f: alloc::vec::Vec<char> = fmt.chars().collect();
let mut year: i32 = 1970;
let mut month: u32 = 1;
let mut day: u32 = 1;
let mut hour: u32 = 0;
let mut minute: u32 = 0;
let mut second: u32 = 0;
let mut micros: u32 = 0;
let mut pm: Option<bool> = None;
let mut has_time = false;
let mut ip = 0usize;
let mut fp = 0usize;
let read_num = |ip: &mut usize, max: usize, inp: &[char]| -> Option<u32> {
let start = *ip;
while *ip < inp.len() && *ip - start < max && inp[*ip].is_ascii_digit() {
*ip += 1;
}
if *ip == start {
return None;
}
inp[start..*ip]
.iter()
.collect::<alloc::string::String>()
.parse()
.ok()
};
while fp < f.len() {
if f[fp] != '%' {
if f[fp].is_whitespace() {
while ip < inp.len() && inp[ip].is_whitespace() {
ip += 1;
}
fp += 1;
continue;
}
if ip < inp.len() && inp[ip] == f[fp] {
ip += 1;
fp += 1;
continue;
}
return Ok(Value::Null);
}
if fp + 1 >= f.len() {
return Ok(Value::Null);
}
let spec = f[fp + 1];
fp += 2;
match spec {
'Y' => match read_num(&mut ip, 4, &inp) {
Some(v) => year = v as i32,
None => return Ok(Value::Null),
},
'y' => match read_num(&mut ip, 2, &inp) {
Some(v) => {
year = if v >= 70 {
1900 + v as i32
} else {
2000 + v as i32
}
}
None => return Ok(Value::Null),
},
'm' | 'c' => match read_num(&mut ip, 2, &inp) {
Some(v @ 1..=12) => month = v,
_ => return Ok(Value::Null),
},
'd' | 'e' => match read_num(&mut ip, 2, &inp) {
Some(v @ 1..=31) => day = v,
_ => return Ok(Value::Null),
},
'H' => match read_num(&mut ip, 2, &inp) {
Some(v @ 0..=23) => {
hour = v;
has_time = true;
}
_ => return Ok(Value::Null),
},
'h' | 'I' => match read_num(&mut ip, 2, &inp) {
Some(v @ 1..=12) => {
hour = v;
has_time = true;
}
_ => return Ok(Value::Null),
},
'i' => match read_num(&mut ip, 2, &inp) {
Some(v @ 0..=59) => {
minute = v;
has_time = true;
}
_ => return Ok(Value::Null),
},
's' | 'S' => match read_num(&mut ip, 2, &inp) {
Some(v @ 0..=59) => {
second = v;
has_time = true;
}
_ => return Ok(Value::Null),
},
'f' => {
let start = ip;
match read_num(&mut ip, 6, &inp) {
Some(v) => {
let ndigits = ip - start;
let mut scaled = v;
for _ in ndigits..6 {
scaled *= 10;
}
micros = scaled;
has_time = true;
}
None => return Ok(Value::Null),
}
}
'p' => {
if ip + 2 <= inp.len() {
let tag: alloc::string::String = inp[ip..ip + 2]
.iter()
.collect::<alloc::string::String>()
.to_ascii_uppercase();
match tag.as_str() {
"AM" => pm = Some(false),
"PM" => pm = Some(true),
_ => return Ok(Value::Null),
}
ip += 2;
has_time = true;
} else {
return Ok(Value::Null);
}
}
'M' | 'b' => {
let rest: alloc::string::String = inp[ip..]
.iter()
.collect::<alloc::string::String>()
.to_ascii_uppercase();
let mut matched = None;
for (idx, name) in MONTH_FULL_UP.iter().enumerate() {
if rest.starts_with(name) {
matched = Some((idx as u32 + 1, name.len()));
break;
}
if rest.starts_with(&name[..3]) {
matched = Some((idx as u32 + 1, 3));
}
}
match matched {
Some((m, len)) => {
month = m;
ip += len;
}
None => return Ok(Value::Null),
}
}
'%' => {
if ip < inp.len() && inp[ip] == '%' {
ip += 1;
} else {
return Ok(Value::Null);
}
}
_ => return Ok(Value::Null),
}
}
if let Some(is_pm) = pm {
hour = match (hour % 12, is_pm) {
(h, true) => h + 12,
(h, false) => h,
};
}
let days = days_from_civil(year, month, day);
if has_time {
let micros_total = i64::from(days) * 86_400_000_000
+ i64::from(hour) * 3_600_000_000
+ i64::from(minute) * 60_000_000
+ i64::from(second) * 1_000_000
+ i64::from(micros);
Ok(Value::Timestamp(micros_total))
} else {
Ok(Value::Date(days))
}
}
pub(super) fn time_format_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("time_format() takes 2 args, got {}", args.len()),
});
}
if matches!(&args[0], Value::Null) || matches!(&args[1], Value::Null) {
return Ok(Value::Null);
}
let day_micros: i64 = match &args[0] {
Value::Timestamp(t) => t.rem_euclid(86_400_000_000),
Value::Text(s) => {
let mut parts = s.trim().splitn(3, ':');
let h: i64 = parts.next().and_then(|x| x.parse().ok()).ok_or_else(|| {
EvalError::TypeMismatch {
detail: format!("time_format(): cannot parse time {s:?}"),
}
})?;
let m: i64 = parts.next().and_then(|x| x.parse().ok()).unwrap_or(0);
let (sec, us) = match parts.next() {
None => (0_i64, 0_i64),
Some(rest) => match rest.split_once('.') {
None => (rest.parse().unwrap_or(0), 0),
Some((s_int, s_frac)) => {
let mut frac = String::from(s_frac);
while frac.len() < 6 {
frac.push('0');
}
frac.truncate(6);
(s_int.parse().unwrap_or(0), frac.parse().unwrap_or(0))
}
},
};
h * 3_600_000_000 + m * 60_000_000 + sec * 1_000_000 + us
}
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"time_format() needs TIME text or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
date_format_mysql(&[Value::Timestamp(day_micros), args[1].clone().into_owned()])
}
fn unit_micros(unit: &str) -> Option<i64> {
Some(match unit {
"microsecond" => 1,
"second" => 1_000_000,
"minute" => 60_000_000,
"hour" => 3_600_000_000,
"day" => 86_400_000_000,
"week" => 7 * 86_400_000_000,
_ => return None,
})
}
pub(super) fn text_or_temporal_micros(v: &Value<'_>, fn_name: &str) -> Result<i64, EvalError> {
ts_of(v, fn_name)
}
fn ts_of(v: &Value<'_>, fn_name: &str) -> Result<i64, EvalError> {
match v {
Value::Timestamp(t) => Ok(*t),
Value::Date(d) => Ok(i64::from(*d) * 86_400_000_000),
Value::Text(s) => parse_text_datetime(s).ok_or_else(|| EvalError::TypeMismatch {
detail: format!("{fn_name}(): cannot parse datetime {s:?}"),
}),
other => Err(EvalError::TypeMismatch {
detail: format!(
"{fn_name}() needs DATE or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn parse_text_datetime(s: &str) -> Option<i64> {
let s = s.trim();
let (date_part, time_part) = match s.split_once(' ') {
Some((d, t)) => (d, Some(t)),
None => match s.split_once('T') {
Some((d, t)) => (d, Some(t)),
None => (s, None),
},
};
let mut dp = date_part.splitn(3, '-');
let y: i32 = dp.next()?.parse().ok()?;
let mo: u32 = dp.next()?.parse().ok()?;
let d: u32 = dp.next()?.parse().ok()?;
if !(1..=12).contains(&mo) || !(1..=31).contains(&d) {
return None;
}
let days = days_from_civil(y, mo, d);
let mut micros = i64::from(days) * 86_400_000_000;
if let Some(t) = time_part {
let mut tp = t.splitn(3, ':');
let h: i64 = tp.next()?.parse().ok()?;
let mi: i64 = tp.next().and_then(|x| x.parse().ok()).unwrap_or(0);
let (sec, us): (i64, i64) = match tp.next() {
None => (0, 0),
Some(rest) => match rest.split_once('.') {
None => (rest.parse().ok()?, 0),
Some((si, sf)) => {
let mut frac = String::from(sf);
while frac.len() < 6 {
frac.push('0');
}
frac.truncate(6);
(si.parse().ok()?, frac.parse().ok()?)
}
},
};
if !(0..=23).contains(&h) || !(0..=59).contains(&mi) || !(0..=59).contains(&sec) {
return None;
}
micros += h * 3_600_000_000 + mi * 60_000_000 + sec * 1_000_000 + us;
}
Some(micros)
}
fn add_months(ts: i64, n: i64) -> i64 {
let days = ts.div_euclid(86_400_000_000);
let day_micros = ts.rem_euclid(86_400_000_000);
let (y, m, d) = civil_from_days(i32::try_from(days).unwrap_or(i32::MAX));
let total = i64::from(y) * 12 + i64::from(m) - 1 + n;
let ny = i32::try_from(total.div_euclid(12)).unwrap_or(i32::MAX);
let nm = u32::try_from(total.rem_euclid(12)).unwrap_or(0) + 1;
let month_len = |y: i32, m: u32| -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ => {
if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
29
} else {
28
}
}
}
};
let nd = d.min(month_len(ny, nm));
i64::from(days_from_civil(ny, nm, nd)) * 86_400_000_000 + day_micros
}
pub(super) fn timestampadd_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 3 {
return Err(EvalError::TypeMismatch {
detail: format!("timestampadd() takes 3 args, got {}", args.len()),
});
}
if args.iter().any(|a| matches!(a, Value::Null)) {
return Ok(Value::Null);
}
let Value::Text(unit) = &args[0] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"timestampadd() unit must be a keyword, got {}",
crate::conversions::pg_type_name_for_error_opt(args[0].data_type())
),
});
};
let n = match &args[1] {
Value::Int(v) => i64::from(*v),
Value::SmallInt(v) => i64::from(*v),
Value::BigInt(v) => *v,
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"timestampadd() count must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let ts = ts_of(&args[2], "timestampadd")?;
let unit_lc = unit.to_ascii_lowercase();
let out = if let Some(per) = unit_micros(&unit_lc) {
ts.saturating_add(n.saturating_mul(per))
} else {
match unit_lc.as_str() {
"month" => add_months(ts, n),
"quarter" => add_months(ts, n.saturating_mul(3)),
"year" => add_months(ts, n.saturating_mul(12)),
other => {
return Err(EvalError::TypeMismatch {
detail: format!("timestampadd(): unknown unit {other:?}"),
});
}
}
};
Ok(Value::Timestamp(out))
}
pub(super) fn timestampdiff_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 3 {
return Err(EvalError::TypeMismatch {
detail: format!("timestampdiff() takes 3 args, got {}", args.len()),
});
}
if args.iter().any(|a| matches!(a, Value::Null)) {
return Ok(Value::Null);
}
let Value::Text(unit) = &args[0] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"timestampdiff() unit must be a keyword, got {}",
crate::conversions::pg_type_name_for_error_opt(args[0].data_type())
),
});
};
let from = ts_of(&args[1], "timestampdiff")?;
let to = ts_of(&args[2], "timestampdiff")?;
let unit_lc = unit.to_ascii_lowercase();
let out = if let Some(per) = unit_micros(&unit_lc) {
(to - from) / per
} else {
let months_between = |from: i64, to: i64| -> i64 {
let sign = if to >= from { 1 } else { -1 };
let (lo, hi) = if sign > 0 { (from, to) } else { (to, from) };
let (ly, lm, _) =
civil_from_days(i32::try_from(lo.div_euclid(86_400_000_000)).unwrap_or(0));
let (hy, hm, _) =
civil_from_days(i32::try_from(hi.div_euclid(86_400_000_000)).unwrap_or(0));
let mut approx =
(i64::from(hy) * 12 + i64::from(hm)) - (i64::from(ly) * 12 + i64::from(lm));
while approx > 0 && add_months(lo, approx) > hi {
approx -= 1;
}
sign * approx
};
match unit_lc.as_str() {
"month" => months_between(from, to),
"quarter" => months_between(from, to) / 3,
"year" => months_between(from, to) / 12,
other => {
return Err(EvalError::TypeMismatch {
detail: format!("timestampdiff(): unknown unit {other:?}"),
});
}
}
};
Ok(Value::BigInt(out))
}
pub(super) fn get_format_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("get_format() takes 2 args, got {}", args.len()),
});
}
if args.iter().any(|a| matches!(a, Value::Null)) {
return Ok(Value::Null);
}
let (Value::Text(kind), Value::Text(region)) = (&args[0], &args[1]) else {
return Err(EvalError::TypeMismatch {
detail: format!(
"get_format() takes (type keyword, region text), got ({:?}, {:?})",
args[0].data_type(),
args[1].data_type()
),
});
};
let fmt = match (
kind.to_ascii_lowercase().as_str(),
region.to_ascii_uppercase().as_str(),
) {
("date", "USA") => "%m.%d.%Y",
("date", "JIS" | "ISO") => "%Y-%m-%d",
("date", "EUR") => "%d.%m.%Y",
("date", "INTERNAL") => "%Y%m%d",
("datetime" | "timestamp", "USA" | "JIS" | "ISO") => "%Y-%m-%d %H.%i.%s",
("datetime" | "timestamp", "EUR") => "%Y-%m-%d %H.%i.%s",
("datetime" | "timestamp", "INTERNAL") => "%Y%m%d%H%i%s",
("time", "USA") => "%h:%i:%s %p",
("time", "JIS" | "ISO") => "%H:%i:%s",
("time", "EUR") => "%H.%i.%s",
("time", "INTERNAL") => "%H%i%s",
_ => return Ok(Value::Null),
};
Ok(Value::text(String::from(fmt)))
}
pub(super) fn timezone_pg(
args: &[Value<'_>],
ctx: &super::EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("timezone() takes 2 args, got {}", args.len()),
});
}
if args.iter().any(|a| matches!(a, Value::Null)) {
return Ok(Value::Null);
}
if let Value::TimeTz { .. } = &args[1] {
return timetz_at_zone(args, ctx);
}
if let Value::Time(us) = &args[1] {
let mut rewritten = args.to_vec();
rewritten[1] = Value::TimeTz {
us: *us,
offset_secs: 0,
};
let out = timetz_at_zone(&rewritten, ctx)?;
if let Value::TimeTz { offset_secs, .. } = &out {
return Ok(Value::TimeTz {
us: *us,
offset_secs: *offset_secs,
});
}
return Ok(out);
}
if let Value::Interval {
months,
days,
micros,
} = &args[0]
{
if *months != 0 || *days != 0 {
return Err(EvalError::TypeMismatch {
detail: "interval time zone must not include months or days".into(),
});
}
let total_min = *micros / 60_000_000;
let (sign, mag) = if total_min < 0 {
('-', -total_min)
} else {
('+', total_min)
};
let spelled = format!("{sign}{:02}:{:02}", mag / 60, mag % 60);
let mut rewritten = args.to_vec();
rewritten[0] = Value::text(spelled);
return timezone_pg(&rewritten, ctx);
}
let Value::Text(zone) = &args[0] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"timezone() zone must be text, got {}",
crate::conversions::pg_type_name_for_error_opt(args[0].data_type())
),
});
};
let z = zone.trim();
let offset = if z.eq_ignore_ascii_case("utc") || z.eq_ignore_ascii_case("gmt") {
0
} else if let Some(off) = parse_tz_offset(z) {
off
} else if let Ok(h) = z.parse::<i64>() {
if h.abs() > 14 {
return Err(EvalError::TypeMismatch {
detail: format!("timezone(): offset {h} out of range"),
});
}
h * 3_600_000_000
} else if let Some(applied) = tz_abbrev_offset(z) {
applied
} else {
let ts = text_or_temporal_micros(&args[1], "timezone")?;
let Some(off) = ctx.zone_offset_at(z, ts) else {
return Err(EvalError::TypeMismatch {
detail: format!(
"timezone({z:?}): time zone not recognized (no tzdata entry \
on this host); use UTC, a fixed abbreviation (EST/PST/JST/\
CET/…), or an explicit '+HH:MM' offset"
),
});
};
return Ok(Value::Timestamp(ts + off));
};
let ts = text_or_temporal_micros(&args[1], "timezone")?;
Ok(Value::Timestamp(ts + offset))
}
fn timetz_at_zone(
args: &[Value<'_>],
ctx: &super::EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let Value::Text(zone) = &args[0] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"timezone() zone must be text, got {}",
crate::conversions::pg_type_name_for_error_opt(args[0].data_type())
),
});
};
let Value::TimeTz { us, offset_secs } = &args[1] else {
unreachable!("caller matched TimeTz");
};
let z = zone.trim();
let now = ctx.clock.map_or(0, |c| c());
let target_secs = if z.eq_ignore_ascii_case("utc") || z.eq_ignore_ascii_case("gmt") {
0
} else if let Some(off) = parse_tz_offset(z) {
(off / 1_000_000) as i32
} else if let Some(off) = tz_abbrev_offset(z).or_else(|| ctx.zone_offset_at(z, now)) {
(off / 1_000_000) as i32
} else {
return Err(EvalError::TypeMismatch {
detail: format!("timezone({z:?}): time zone not recognized"),
});
};
const DAY: i64 = 86_400_000_000;
let delta_us = (i64::from(target_secs) - i64::from(*offset_secs)) * 1_000_000;
let new_us = (us + delta_us).rem_euclid(DAY);
Ok(Value::TimeTz {
us: new_us,
offset_secs: target_secs,
})
}
pub(crate) fn resolve_zone_offset(z: &str) -> Option<i64> {
let z = z.trim();
if z.is_empty() || z.eq_ignore_ascii_case("utc") || z.eq_ignore_ascii_case("gmt") {
return Some(0);
}
if let Some(off) = parse_tz_offset(z) {
return Some(off);
}
if let Ok(h) = z.parse::<i64>() {
return (h.abs() <= 14).then_some(h * 3_600_000_000);
}
tz_abbrev_offset(z).map(|applied| -applied)
}
fn tz_abbrev_offset(z: &str) -> Option<i64> {
const H: i64 = 3_600_000_000;
let m = |h: i64, min: i64| h * H + min * 60_000_000;
let off = match z.to_ascii_uppercase().as_str() {
"EST" => m(5, 0),
"EDT" => m(4, 0),
"CST" => m(6, 0),
"CDT" => m(5, 0),
"MST" => m(7, 0),
"MDT" => m(6, 0),
"PST" => m(8, 0),
"PDT" => m(7, 0),
"AKST" => m(9, 0),
"AKDT" => m(8, 0),
"HST" => m(10, 0),
"JST" | "KST" => m(-9, 0),
"CET" | "WEST" => m(-1, 0),
"CEST" | "EET" | "SAST" => m(-2, 0),
"EEST" | "MSK" => m(-3, 0),
"WET" | "GMT" | "UTC" => 0,
"BST" | "IST_IE" => m(-1, 0),
"AEST" => m(-10, 0),
"AEDT" => m(-11, 0),
"NZST" => m(-12, 0),
"ACST" => m(-9, -30),
_ => return None,
};
Some(off)
}
fn parse_tz_offset(s: &str) -> Option<i64> {
let s = s.trim();
let (sign, rest) = match s.as_bytes().first()? {
b'+' => (1_i64, &s[1..]),
b'-' => (-1_i64, &s[1..]),
_ => return None,
};
let (h, m) = rest.split_once(':')?;
let h: i64 = h.parse().ok()?;
let m: i64 = m.parse().ok()?;
if h > 14 || m > 59 {
return None;
}
Some(sign * (h * 3_600_000_000 + m * 60_000_000))
}
pub(super) fn convert_tz_mysql(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 3 {
return Err(EvalError::TypeMismatch {
detail: format!("convert_tz() takes 3 args, got {}", args.len()),
});
}
if args.iter().any(|a| matches!(a, Value::Null)) {
return Ok(Value::Null);
}
let ts = text_or_temporal_micros(&args[0], "convert_tz")?;
let (Value::Text(from), Value::Text(to)) = (&args[1], &args[2]) else {
return Err(EvalError::TypeMismatch {
detail: format!(
"convert_tz() timezones must be text, got ({:?}, {:?})",
args[1].data_type(),
args[2].data_type()
),
});
};
let (Some(from_off), Some(to_off)) = (parse_tz_offset(from), parse_tz_offset(to)) else {
return Ok(Value::Null);
};
Ok(Value::Timestamp(ts - from_off + to_off))
}