use rupg_common::{Error, Result, SqlState};
use rupg_types::{Snapshot, Value};
use crate::{Call, Kernel, bad_value, not_yet};
const FIRST_NORMAL_XID: u32 = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum XactState {
InProgress,
Committed,
Aborted,
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"pg_snapshot_xmin" => snapshot_xmin,
"pg_snapshot_xmax" => snapshot_xmax,
"pg_visible_in_snapshot" => visible_in_snapshot,
"pg_current_xact_id" => current_xact_id,
"pg_current_xact_id_if_assigned" => current_xact_id_if_assigned,
"pg_current_snapshot" => current_snapshot,
"pg_xact_status" => xact_status,
"xid_age" => xid_age,
"mxid_age" => mxid_age,
_ => return None,
})
}
const FIRST_MULTI_XACT_ID: u32 = 1;
fn xid_age(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(Value::Oid(xid)) = args.first() else { return Err(bad_value()) };
if *xid < FIRST_NORMAL_XID {
return Ok(Value::Int4(i32::MAX));
}
Ok(Value::Int4(call.session.stable_transaction_id().wrapping_sub(*xid).cast_signed()))
}
fn mxid_age(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(Value::Oid(xid)) = args.first() else { return Err(bad_value()) };
if *xid == 0 {
return Ok(Value::Int4(i32::MAX));
}
Ok(Value::Int4(FIRST_MULTI_XACT_ID.wrapping_sub(*xid).cast_signed()))
}
fn snapshot(args: &[Value], n: usize) -> Result<Snapshot> {
match args.get(n) {
Some(Value::Bytea(v)) => Snapshot::from_bytes(v).ok_or_else(bad_value),
_ => Err(bad_value()),
}
}
fn snapshot_xmin(_: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(Value::Int8(snapshot(args, 0)?.xmin.cast_signed()))
}
fn snapshot_xmax(_: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(Value::Int8(snapshot(args, 0)?.xmax.cast_signed()))
}
fn visible_in_snapshot(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(Value::Int8(xid)) = args.first() else { return Err(bad_value()) };
Ok(Value::Bool(snapshot(args, 1)?.is_visible(xid.cast_unsigned())))
}
pub(crate) fn snapshot_xip(_: &Call<'_>, args: &[Value]) -> Result<Vec<Vec<Value>>> {
let snapshot = snapshot(args, 0)?;
Ok(snapshot.xip.iter().map(|xid| vec![Value::Int8(xid.cast_signed())]).collect())
}
fn current_xact_id(call: &Call<'_>, _: &[Value]) -> Result<Value> {
let Some(xid) = call.session.transaction_id(true) else {
return Err(not_yet("a transaction ID in a session with no server"));
};
Ok(Value::Int8(xid.cast_signed()))
}
fn current_xact_id_if_assigned(call: &Call<'_>, _: &[Value]) -> Result<Value> {
Ok(call.session.transaction_id(false).map_or(Value::Null, |xid| Value::Int8(xid.cast_signed())))
}
fn current_snapshot(call: &Call<'_>, _: &[Value]) -> Result<Value> {
let Some(snapshot) = call.session.current_snapshot() else {
return Err(Error::internal("no active snapshot set"));
};
Ok(Value::Bytea(snapshot.to_bytes()))
}
fn xact_status(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(Value::Int8(full)) = args.first() else { return Err(bad_value()) };
let full = full.cast_unsigned();
let xid = u32::try_from(full & u64::from(u32::MAX)).unwrap_or_default();
if xid == 0 {
return Ok(Value::Null);
}
if xid < FIRST_NORMAL_XID {
return Ok(Value::Text("committed".into()));
}
if full >= call.session.next_transaction_id() {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
format!("transaction ID {full} is in the future"),
));
}
Ok(match call.session.transaction_state(xid) {
None => Value::Null,
Some(XactState::InProgress) => Value::Text("in progress".into()),
Some(XactState::Committed) => Value::Text("committed".into()),
Some(XactState::Aborted) => Value::Text("aborted".into()),
})
}