use std::rc::Rc;
use rupg_common::{Error, Result, SqlState};
use rupg_types::{
DATE_INFINITY, DATE_NEGATIVE_INFINITY, Interval, Numeric, NumericSign, POSTGRES_EPOCH_JDATE,
TIMESTAMP_INFINITY, TIMESTAMP_NEGATIVE_INFINITY, TimeZone, UNIX_EPOCH_JDATE, USECS_PER_DAY,
USECS_PER_SEC, Value, date2j, float8_out, j2date, local_offset,
};
use crate::cast::{
TIMESTAMP_END_JULIAN, infinite, instant, local, timestamp_range, valid_timestamp,
};
use crate::compare::span;
use crate::datetime::{days_of_month, timestamp_plus};
use crate::math::valid_date;
use crate::timezone::{lookup_timezone, parse_sane_timezone};
use crate::{Call, Kernel, bad_value};
const UNIX_TO_POSTGRES_SECS: i64 = 946_684_800;
const CHECK_EVERY: usize = 4096;
fn int_arg(args: &[Value], at: usize) -> Result<i32> {
match args.get(at) {
Some(Value::Int4(n)) => Ok(*n),
_ => Err(bad_value()),
}
}
fn float_arg(args: &[Value], at: usize) -> Result<f64> {
match args.get(at) {
Some(Value::Float8(f)) => Ok(*f),
_ => Err(bad_value()),
}
}
pub(crate) fn format_g(v: f64) -> String {
let mut out = Vec::new();
float8_out(v, -9, &mut out);
String::from_utf8(out).unwrap_or_default()
}
fn format_02g(v: f64) -> String {
let text = format_g(v);
if text.len() == 1 { format!("0{text}") } else { text }
}
pub(crate) fn valid_julian(year: i32, mon: i32) -> bool {
(year > -4713 || (year == -4713 && mon >= 11))
&& (year < 5_874_898 || (year == 5_874_898 && mon < 6))
}
fn validate_date(year: &mut i32, mon: i32, mday: i32, bc: bool) -> bool {
if *year <= 0 {
return false;
}
if bc {
*year = -(*year - 1);
}
(1..=12).contains(&mon) && (1..=31).contains(&mday) && mday <= days_of_month(*year, mon)
}
fn field_overflow(message: String) -> Error {
Error::new(SqlState::DATETIME_FIELD_OVERFLOW, message)
}
fn value_range(message: String) -> Error {
Error::new(SqlState::DATETIME_VALUE_OUT_OF_RANGE, message)
}
fn make_date(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let (mut year, mon, mday) = (int_arg(args, 0)?, int_arg(args, 1)?, int_arg(args, 2)?);
let bc = year < 0;
if bc {
year = year.checked_neg().ok_or_else(|| {
field_overflow(format!("date field value out of range: {year}-{mon:02}-{mday:02}"))
})?;
}
if !validate_date(&mut year, mon, mday, bc) {
return Err(field_overflow(format!(
"date field value out of range: {year}-{mon:02}-{mday:02}"
)));
}
let out_of_range = || value_range(format!("date out of range: {year}-{mon:02}-{mday:02}"));
if !valid_julian(year, mon) {
return Err(out_of_range());
}
let date = date2j(year, mon, mday) - POSTGRES_EPOCH_JDATE;
if !valid_date(i64::from(date)) {
return Err(out_of_range());
}
Ok(Value::Date(date))
}
fn time_overflows(hour: i32, min: i32, sec: f64) -> bool {
if !(0..=24).contains(&hour) || !(0..60).contains(&min) || sec.is_nan() {
return true;
}
let sec = (sec * 1e6).round_ties_even();
if !(0.0..=60e6).contains(&sec) {
return true;
}
time_of(hour, min, sec) > USECS_PER_DAY
}
#[allow(clippy::cast_possible_truncation)]
fn time_of(hour: i32, min: i32, micros: f64) -> i64 {
(i64::from(hour) * 60 + i64::from(min)) * 60 * USECS_PER_SEC + micros as i64
}
fn make_time(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let (hour, min, sec) = (int_arg(args, 0)?, int_arg(args, 1)?, float_arg(args, 2)?);
if time_overflows(hour, min, sec) {
return Err(field_overflow(format!(
"time field value out of range: {hour}:{min:02}:{}",
format_02g(sec)
)));
}
Ok(Value::Time(time_of(hour, min, (sec * 1e6).round_ties_even())))
}
fn timestamp_of(args: &[Value]) -> Result<i64> {
let (year, mon, mday) = (int_arg(args, 0)?, int_arg(args, 1)?, int_arg(args, 2)?);
let (hour, min, sec) = (int_arg(args, 3)?, int_arg(args, 4)?, float_arg(args, 5)?);
let bc = year < 0;
let mut internal = if bc { year.wrapping_neg() } else { year };
if !validate_date(&mut internal, mon, mday, bc) {
return Err(field_overflow(format!(
"date field value out of range: {year}-{mon:02}-{mday:02}"
)));
}
if !valid_julian(internal, mon) {
return Err(value_range(format!("date out of range: {year}-{mon:02}-{mday:02}")));
}
let date = i64::from(date2j(internal, mon, mday) - POSTGRES_EPOCH_JDATE);
if time_overflows(hour, min, sec) {
return Err(field_overflow(format!(
"time field value out of range: {hour}:{min:02}:{}",
format_02g(sec)
)));
}
let time = time_of(hour, min, (sec * 1e6).round_ties_even());
date.checked_mul(USECS_PER_DAY)
.and_then(|t| t.checked_add(time))
.filter(|&t| valid_timestamp(t))
.ok_or_else(|| {
value_range(format!(
"timestamp out of range: {year}-{mon:02}-{mday:02} {hour}:{min:02}:{}",
format_02g(sec)
))
})
}
fn make_timestamp(_: &Call<'_>, args: &[Value]) -> Result<Value> {
timestamp_of(args).map(Value::Timestamp)
}
fn make_timestamptz(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let utc = instant(call.session, timestamp_of(args)?)?;
if !valid_timestamp(utc) {
return Err(timestamp_range());
}
Ok(Value::TimestampTz(utc))
}
fn make_timestamptz_at_timezone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let timestamp = timestamp_of(args)?;
let zone = args.get(6).ok_or_else(bad_value)?;
let west = parse_sane_timezone(call, timestamp, zone)?;
timestamp
.checked_add(i64::from(west) * USECS_PER_SEC)
.filter(|&t| valid_timestamp(t))
.map(Value::TimestampTz)
.ok_or_else(timestamp_range)
}
fn finite(_: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(Value::Bool(match args.first() {
Some(Value::Date(d)) => *d != DATE_INFINITY && *d != DATE_NEGATIVE_INFINITY,
Some(Value::Timestamp(t) | Value::TimestampTz(t)) => !infinite(*t),
Some(Value::Interval(i)) => *i != Interval::INFINITY && *i != Interval::NEGATIVE_INFINITY,
_ => return Err(bad_value()),
}))
}
fn time_timetz(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(Value::Time(time)) = args.first() else { return Err(bad_value()) };
let today = local(call.session, call.session.transaction_start())?.div_euclid(USECS_PER_DAY);
let seconds = today * 86_400 + time / USECS_PER_SEC + UNIX_TO_POSTGRES_SECS;
let (west, _) = local_offset(seconds, call.session.zone()?.as_ref());
Ok(Value::TimeTz(*time, west))
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn timeofday(call: &Call<'_>, _: &[Value]) -> Result<Value> {
const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS: [&str; 12] =
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
let now = call.session.clock();
let unix = now.div_euclid(USECS_PER_SEC) + UNIX_TO_POSTGRES_SECS;
let zone = call.session.zone()?;
let (east, abbrev) = zone.at(unix);
let local = unix + i64::from(east);
let julian = (local.div_euclid(86_400) + i64::from(UNIX_EPOCH_JDATE)) as i32;
let (year, mon, mday) = j2date(julian);
let secs = local.rem_euclid(86_400);
Ok(Value::text(format!(
"{} {} {mday:02} {:02}:{:02}:{:02}.{:06} {year} {abbrev}",
DAYS[(julian + 1).rem_euclid(7) as usize],
MONTHS[mon as usize - 1],
secs / 3600,
secs / 60 % 60,
secs % 60,
now.rem_euclid(USECS_PER_SEC),
)))
}
#[allow(clippy::cast_possible_truncation)]
fn float8_timestamptz(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let seconds = float_arg(args, 0)?;
if seconds.is_nan() {
return Err(value_range("timestamp cannot be NaN".to_owned()));
}
if seconds.is_infinite() {
let result = if seconds < 0.0 { TIMESTAMP_NEGATIVE_INFINITY } else { TIMESTAMP_INFINITY };
return Ok(Value::TimestampTz(result));
}
let range = || value_range(format!("timestamp out of range: \"{}\"", format_g(seconds)));
let low = 86_400.0 * f64::from(-UNIX_EPOCH_JDATE);
let high = 86_400.0 * f64::from(TIMESTAMP_END_JULIAN - UNIX_EPOCH_JDATE);
if seconds < low || seconds >= high {
return Err(range());
}
let shifted = seconds - f64::from(POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86_400.0;
let result = (shifted * 1e6).round_ties_even() as i64;
if !valid_timestamp(result) {
return Err(range());
}
Ok(Value::TimestampTz(result))
}
fn step_arg(v: Option<&Value>) -> Result<Interval> {
let Some(Value::Interval(step)) = v else { return Err(bad_value()) };
if span(step) == 0 {
return Err(Error::new(SqlState::INVALID_PARAMETER_VALUE, "step size cannot equal zero"));
}
if *step == Interval::INFINITY || *step == Interval::NEGATIVE_INFINITY {
return Err(Error::new(SqlState::INVALID_PARAMETER_VALUE, "step size cannot be infinite"));
}
Ok(*step)
}
fn timestamp_series(
call: &Call<'_>,
args: &[Value],
zone: Option<&dyn TimeZone>,
kind: fn(i64) -> Value,
) -> Result<Vec<Vec<Value>>> {
let (
Some(Value::Timestamp(start) | Value::TimestampTz(start)),
Some(Value::Timestamp(finish) | Value::TimestampTz(finish)),
) = (args.first(), args.get(1))
else {
return Err(bad_value());
};
let step = step_arg(args.get(2))?;
let up = span(&step) > 0;
let (mut current, finish) = (*start, *finish);
let most = call.want.unwrap_or(usize::MAX);
let mut rows = Vec::new();
while rows.len() < most && if up { current <= finish } else { current >= finish } {
if rows.len() % CHECK_EVERY == CHECK_EVERY - 1 {
call.session.interrupted()?;
}
rows.push(vec![kind(current)]);
current = timestamp_plus(current, &step, zone)?;
}
Ok(rows)
}
fn series_timestamp(call: &Call<'_>, args: &[Value]) -> Result<Vec<Vec<Value>>> {
timestamp_series(call, args, None, Value::Timestamp)
}
fn series_timestamptz(call: &Call<'_>, args: &[Value]) -> Result<Vec<Vec<Value>>> {
let zone: Rc<dyn TimeZone> = match args.get(3) {
Some(name) => lookup_timezone(call, name)?,
None => call.session.zone()?,
};
timestamp_series(call, args, Some(zone.as_ref()), Value::TimestampTz)
}
fn numeric_arg(args: &[Value], at: usize) -> Result<&Numeric> {
match args.get(at) {
Some(Value::Numeric(n)) => Ok(n),
_ => Err(bad_value()),
}
}
fn special(what: &str, n: &Numeric) -> Result<()> {
let name = match n.sign() {
NumericSign::NaN => "NaN",
NumericSign::Infinity | NumericSign::NegativeInfinity => "infinity",
_ => return Ok(()),
};
Err(Error::new(SqlState::INVALID_PARAMETER_VALUE, format!("{what} cannot be {name}")))
}
fn series_numeric(call: &Call<'_>, args: &[Value]) -> Result<Vec<Vec<Value>>> {
let (start, stop) = (numeric_arg(args, 0)?, numeric_arg(args, 1)?);
special("start value", start)?;
special("stop value", stop)?;
let step = match args.get(2) {
Some(_) => {
let step = numeric_arg(args, 2)?;
special("step size", step)?;
if step.digits().is_empty() {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
"step size cannot equal zero",
));
}
step.clone()
}
None => Numeric::from_integer(1),
};
let up = step.sign() == NumericSign::Positive;
let mut current = start.clone();
let most = call.want.unwrap_or(usize::MAX);
let mut rows = Vec::new();
while rows.len() < most {
let order = current.compare(stop);
if (up && order.is_gt()) || (!up && order.is_lt()) {
break;
}
if rows.len() % CHECK_EVERY == CHECK_EVERY - 1 {
call.session.interrupted()?;
}
let next = current.add(&step).map_err(crate::type_error)?;
rows.push(vec![Value::Numeric(current)]);
current = next;
}
Ok(rows)
}
pub(crate) fn set_kernel(src: &str) -> Option<crate::srf::SetKernel> {
Some(match src {
"generate_series_timestamp" => series_timestamp,
"generate_series_timestamptz" | "generate_series_timestamptz_at_zone" => series_timestamptz,
"generate_series_numeric" | "generate_series_step_numeric" => series_numeric,
_ => return None,
})
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"make_date" => make_date,
"make_time" => make_time,
"make_timestamp" => make_timestamp,
"make_timestamptz" => make_timestamptz,
"make_timestamptz_at_timezone" => make_timestamptz_at_timezone,
"date_finite" | "timestamp_finite" | "interval_finite" => finite,
"time_timetz" => time_timetz,
"timeofday" => timeofday,
"float8_timestamptz" => float8_timestamptz,
_ => return None,
})
}