use std::cell::{Cell, RefCell};
use rupg_common::{Error, Result, SqlState};
use rupg_types::prng::Prng;
use rupg_types::{
DATE_INFINITY, DATE_NEGATIVE_INFINITY, Interval, POSTGRES_EPOCH_JDATE, TIMESTAMP_INFINITY,
TIMESTAMP_NEGATIVE_INFINITY, UNIX_TO_POSTGRES_USECS, Value,
};
use crate::calendar::format_g;
use crate::datetime::timestamp_plus;
use crate::{Call, Kernel, Session, bad_value};
const NS_PER_MS: i64 = 1_000_000;
const NS_PER_US: i64 = 1000;
const US_PER_MS: i64 = 1000;
const UNIX_OFFSET_US: i64 = -UNIX_TO_POSTGRES_USECS;
const SUBMS_MINIMAL_STEP_NS: i64 = NS_PER_MS / (1 << 12) + 1;
const GREGORIAN_EPOCH_JDATE: i64 = 2_299_161;
#[derive(Debug, Default)]
pub struct Randomness {
prng: Option<Prng>,
global: Option<Prng>,
previous_ns: i64,
}
thread_local! {
static LOCAL: RefCell<Randomness> = RefCell::new(Randomness::default());
static BYTES: Cell<Option<Prng>> = const { Cell::new(None) };
}
#[allow(clippy::cast_sign_loss)]
pub(crate) fn clock_random(clock: i64, buf: &mut [u8]) {
BYTES.with(|cell| {
let mut prng = cell.get().unwrap_or_else(|| Prng::seed(clock as u64));
for chunk in buf.chunks_mut(8) {
chunk.copy_from_slice(&prng.next_u64().to_ne_bytes()[..chunk.len()]);
}
cell.set(Some(prng));
});
}
fn with_state<T>(session: &dyn Session, f: impl FnOnce(&mut Randomness) -> T) -> T {
match session.randomness() {
Some(cell) => f(&mut cell.borrow_mut()),
None => LOCAL.with(|cell| f(&mut cell.borrow_mut())),
}
}
fn strong_seed(session: &dyn Session) -> Prng {
let mut bytes = [0; 16];
session.strong_random(&mut bytes);
Prng::from_bytes(bytes)
}
fn with_prng<T>(session: &dyn Session, f: impl FnOnce(&mut Prng) -> T) -> T {
with_state(session, |state| f(state.prng.get_or_insert_with(|| strong_seed(session))))
}
pub(crate) fn global_range(session: &dyn Session, rmin: u64, rmax: u64) -> u64 {
with_state(session, |state| {
state.global.get_or_insert_with(|| strong_seed(session)).uint64_range(rmin, rmax)
})
}
fn parameter_error(message: &str) -> Error {
Error::new(SqlState::INVALID_PARAMETER_VALUE, message)
}
fn check_bounds<T: PartialOrd>(rmin: T, rmax: T) -> Result<()> {
if rmin > rmax {
return Err(parameter_error("lower bound must be less than or equal to upper bound"));
}
Ok(())
}
fn finite_bounds(finite: bool) -> Result<()> {
if finite { Ok(()) } else { Err(parameter_error("lower and upper bounds must be finite")) }
}
fn setseed(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [Value::Float8(seed)] = args else { return Err(bad_value()) };
let seed = *seed;
if !(-1.0..=1.0).contains(&seed) {
return Err(parameter_error(&format!(
"setseed parameter {} is out of allowed range [-1,1]",
format_g(seed)
)));
}
with_state(call.session, |state| state.prng = Some(Prng::fseed(seed)));
Ok(Value::Text(String::new()))
}
fn drandom(call: &Call<'_>, _: &[Value]) -> Result<Value> {
Ok(Value::Float8(with_prng(call.session, Prng::double)))
}
fn drandom_normal(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [Value::Float8(mean), Value::Float8(stddev)] = args else { return Err(bad_value()) };
let z = with_prng(call.session, Prng::double_normal);
Ok(Value::Float8(stddev * z + mean))
}
#[allow(clippy::cast_possible_truncation)]
fn int_random(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let range = |rmin: i64, rmax: i64| -> Result<i64> {
check_bounds(rmin, rmax)?;
Ok(with_prng(call.session, |prng| prng.int64_range(rmin, rmax)))
};
Ok(match args {
[Value::Int4(a), Value::Int4(b)] => Value::Int4(range((*a).into(), (*b).into())? as i32),
[Value::Int8(a), Value::Int8(b)] => Value::Int8(range(*a, *b)?),
[Value::Date(a), Value::Date(b)] => {
check_bounds(a, b)?;
finite_bounds(*a != DATE_NEGATIVE_INFINITY && *b != DATE_INFINITY)?;
Value::Date(range((*a).into(), (*b).into())? as i32)
}
[Value::Timestamp(a), Value::Timestamp(b)]
| [Value::TimestampTz(a), Value::TimestampTz(b)] => {
check_bounds(a, b)?;
finite_bounds(*a != TIMESTAMP_NEGATIVE_INFINITY && *b != TIMESTAMP_INFINITY)?;
let result = range(*a, *b)?;
match args[0] {
Value::Timestamp(_) => Value::Timestamp(result),
_ => Value::TimestampTz(result),
}
}
_ => return Err(bad_value()),
})
}
fn numeric_random(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [Value::Numeric(rmin), Value::Numeric(rmax)] = args else { return Err(bad_value()) };
let result = with_prng(call.session, |prng| rupg_types::Numeric::random(prng, rmin, rmax))?;
Ok(Value::Numeric(result))
}
fn set_version(uuid: &mut [u8; 16], version: u8) {
uuid[6] = (uuid[6] & 0x0f) | (version << 4);
uuid[8] = (uuid[8] & 0x3f) | 0x80;
}
fn gen_random_uuid(call: &Call<'_>, _: &[Value]) -> Result<Value> {
let mut uuid = [0; 16];
call.session.strong_random(&mut uuid);
set_version(&mut uuid, 4);
Ok(Value::Uuid(uuid))
}
fn time_ns_ascending(session: &dyn Session) -> i64 {
let mut ns = (session.clock() + UNIX_OFFSET_US) * NS_PER_US;
with_state(session, |state| {
if state.previous_ns + SUBMS_MINIMAL_STEP_NS >= ns {
ns = state.previous_ns + SUBMS_MINIMAL_STEP_NS;
}
state.previous_ns = ns;
});
ns
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn generate_uuidv7(session: &dyn Session, unix_ts_ms: i64, sub_ms: i64) -> Value {
let mut uuid = [0; 16];
let ms = (unix_ts_ms as u64).to_be_bytes();
uuid[..6].copy_from_slice(&ms[2..]);
let precision = (sub_ms * (1 << 12) / NS_PER_MS) as u32;
uuid[6] = (precision >> 8) as u8;
uuid[7] = precision as u8;
session.strong_random(&mut uuid[8..]);
set_version(&mut uuid, 7);
Value::Uuid(uuid)
}
fn uuidv7(call: &Call<'_>, _: &[Value]) -> Result<Value> {
let ns = time_ns_ascending(call.session);
Ok(generate_uuidv7(call.session, ns / NS_PER_MS, ns % NS_PER_MS))
}
fn uuidv7_interval(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [Value::Interval(shift)] = args else { return Err(bad_value()) };
if *shift == Interval::INFINITY || *shift == Interval::NEGATIVE_INFINITY {
return Err(Error::new(
SqlState::DATETIME_VALUE_OUT_OF_RANGE,
"interval out of range for UUID version 7",
)
.with_detail("UUID version 7 does not support infinite intervals."));
}
let ns = time_ns_ascending(call.session);
let zone = call.session.zone()?;
let ts = timestamp_plus(ns / NS_PER_US - UNIX_OFFSET_US, shift, Some(zone.as_ref()))?;
let max = ((1i64 << 48) - 1) * US_PER_MS - UNIX_OFFSET_US;
if ts < -UNIX_OFFSET_US || ts > max {
return Err(Error::new(
SqlState::DATETIME_VALUE_OUT_OF_RANGE,
"timestamp out of range for UUID version 7",
)
.with_detail(
"UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889.",
));
}
let us = ts + UNIX_OFFSET_US;
Ok(generate_uuidv7(call.session, us / US_PER_MS, (us % US_PER_MS) * NS_PER_US + ns % NS_PER_US))
}
fn uuid_arg(args: &[Value]) -> Result<&[u8; 16]> {
match args {
[Value::Uuid(uuid)] => Ok(uuid),
_ => Err(bad_value()),
}
}
fn uuid_extract_version(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let uuid = uuid_arg(args)?;
if uuid[8] & 0xc0 != 0x80 {
return Ok(Value::Null);
}
Ok(Value::Int2(i16::from(uuid[6] >> 4)))
}
#[allow(clippy::cast_possible_wrap)]
fn uuid_extract_timestamp(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let uuid = uuid_arg(args)?;
if uuid[8] & 0xc0 != 0x80 {
return Ok(Value::Null);
}
let byte = |at: usize, shift: u32| u64::from(uuid[at]) << shift;
Ok(match uuid[6] >> 4 {
1 => {
let tms = byte(0, 24)
+ byte(1, 16)
+ byte(2, 8)
+ byte(3, 0)
+ byte(4, 40)
+ byte(5, 32)
+ (u64::from(uuid[6] & 0xf) << 56)
+ byte(7, 48);
let offset =
(i64::from(POSTGRES_EPOCH_JDATE) - GREGORIAN_EPOCH_JDATE) * 86_400 * 1_000_000;
Value::TimestampTz((tms / 10) as i64 - offset)
}
7 => {
let tms =
byte(5, 0) + byte(4, 8) + byte(3, 16) + byte(2, 24) + byte(1, 32) + byte(0, 40);
Value::TimestampTz((tms * US_PER_MS as u64) as i64 - UNIX_OFFSET_US)
}
_ => Value::Null,
})
}
fn bytea_uuid(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let [Value::Bytea(bytes)] = args else { return Err(bad_value()) };
let uuid = <[u8; 16]>::try_from(bytes.as_slice()).map_err(|_| {
Error::new(SqlState::INVALID_BINARY_REPRESENTATION, "invalid input length for type uuid")
.with_detail(format!("Expected 16 bytes, got {}.", bytes.len()))
})?;
Ok(Value::Uuid(uuid))
}
fn uuid_bytea(_: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(Value::Bytea(uuid_arg(args)?.to_vec()))
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"setseed" => setseed,
"drandom" => drandom,
"drandom_normal" => drandom_normal,
"int4random" | "int8random" | "date_random" | "timestamp_random" | "timestamptz_random" => {
int_random
}
"numeric_random" => numeric_random,
"gen_random_uuid" => gen_random_uuid,
"uuidv7" => uuidv7,
"uuidv7_interval" => uuidv7_interval,
"uuid_extract_version" => uuid_extract_version,
"uuid_extract_timestamp" => uuid_extract_timestamp,
"bytea_uuid" => bytea_uuid,
"uuid_bytea" => uuid_bytea,
_ => return None,
})
}