use crate::error::{TViewError, TViewResult};
use pgrx::datum::{DatumWithOid, FromDatum, IntoDatum};
use pgrx::pg_sys::Oid;
use pgrx::prelude::*;
use pgrx::spi::{SpiClient, SpiHeapTupleData, SpiTupleTable};
pub trait Text: IntoDatum {}
impl Text for &str {}
impl Text for String {}
impl Text for &String {}
impl<T: Text> Text for Option<T> {}
pub trait OidValue: IntoDatum {}
impl OidValue for Oid {}
impl OidValue for Option<Oid> {}
pub trait JsonbValue: IntoDatum {}
impl JsonbValue for pgrx::JsonB {}
impl JsonbValue for Option<pgrx::JsonB> {}
pub fn text<'a, T: Text>(value: T) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn text_array<'a, T: Text>(values: Vec<T>) -> DatumWithOid<'a> {
DatumWithOid::from(values)
}
pub fn text_array_of<'a>(values: &[String]) -> DatumWithOid<'a> {
DatumWithOid::from(values.to_vec())
}
pub fn oid<'a, T: OidValue>(value: T) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn regclass<'a>(relid: Oid) -> DatumWithOid<'a> {
unsafe { DatumWithOid::new(relid, pgrx::pg_sys::REGCLASSOID) }
}
pub fn oid_array<'a>(values: Vec<Oid>) -> DatumWithOid<'a> {
DatumWithOid::from(values)
}
pub fn int8<'a>(value: i64) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn int8_array<'a>(values: Vec<i64>) -> DatumWithOid<'a> {
DatumWithOid::from(values)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LookupKeys {
One(i64),
Many(Vec<i64>),
}
impl LookupKeys {
#[must_use]
pub fn of(mut keys: Vec<i64>) -> Option<Self> {
match keys.len() {
0 => None,
1 => keys.pop().map(Self::One),
_ => Some(Self::Many(keys)),
}
}
#[must_use]
pub fn predicate(&self, column: &str, param: usize) -> String {
match self {
Self::One(_) => format!("{column} = ${param}::pg_catalog.int8"),
Self::Many(_) => format!("{column} = ANY (${param}::pg_catalog.int8[])"),
}
}
#[must_use]
pub fn arg<'a>(&self) -> DatumWithOid<'a> {
match self {
Self::One(key) => int8(*key),
Self::Many(keys) => int8_array(keys.clone()),
}
}
#[must_use]
pub const fn plan(&self) -> Plan {
match self {
Self::One(_) => Plan::Generic,
Self::Many(_) => Plan::PerExecution,
}
}
}
pub fn int4<'a>(value: i32) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn int2<'a>(value: i16) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn int2_array<'a>(values: Vec<i16>) -> DatumWithOid<'a> {
DatumWithOid::from(values)
}
pub fn boolean<'a>(value: bool) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn jsonb<'a, T: JsonbValue>(value: T) -> DatumWithOid<'a> {
DatumWithOid::from(value)
}
pub fn error(query: &str, e: &impl std::fmt::Display) -> TViewError {
TViewError::SpiError {
query: query.to_string(),
error: e.to_string(),
}
}
pub fn catalog_error(operation: &str, e: &impl std::fmt::Display) -> TViewError {
TViewError::CatalogError {
operation: operation.to_string(),
pg_error: e.to_string(),
}
}
pub fn strings(sql: &str, args: &[DatumWithOid<'_>]) -> TViewResult<Vec<String>> {
Ok(rows(sql, args, |row| Ok(row.get::<String>(1)?))?
.into_iter()
.flatten()
.collect())
}
pub fn oids(sql: &str, args: &[DatumWithOid<'_>]) -> TViewResult<Vec<Oid>> {
Ok(rows(sql, args, |row| Ok(row.get::<Oid>(1)?))?
.into_iter()
.flatten()
.collect())
}
pub fn one<T: FromDatum + IntoDatum>(
sql: &str,
args: &[DatumWithOid<'_>],
) -> TViewResult<Option<T>> {
match Spi::get_one_with_args::<T>(sql, args) {
Ok(value) => Ok(value),
Err(pgrx::spi::Error::InvalidPosition) => Ok(None),
Err(e) => Err(error(sql, &e)),
}
}
pub fn run(sql: &str, args: &[DatumWithOid<'_>]) -> TViewResult<()> {
Spi::run_with_args(sql, args).map_err(|e| error(sql, &e))
}
pub fn count_rows(table_oid: Oid) -> TViewResult<u64> {
let sql = format!(
"SELECT pg_catalog.count(*) FROM {}",
crate::utils::qualified_relname_from_oid(table_oid)?
);
let count = Spi::get_one::<i64>(&sql).map_err(|e| error(&sql, &e))?;
Ok(count.unwrap_or(0).unsigned_abs())
}
pub fn run_counted(sql: &str) -> TViewResult<u64> {
Spi::connect_mut(|client| client.update(sql, None, &[]).map(|done| done.len() as u64))
.map_err(|e| error(sql, &e))
}
pub fn rows<T>(
sql: &str,
args: &[DatumWithOid<'_>],
mut read: impl FnMut(&SpiHeapTupleData<'_>) -> TViewResult<T>,
) -> TViewResult<Vec<T>> {
Spi::connect(|client| {
let mut out = Vec::new();
for row in client.select(sql, None, args).map_err(|e| error(sql, &e))? {
out.push(read(&row)?);
}
Ok(out)
})
}
pub fn run_ddl(sql: &str) -> TViewResult<()> {
super::spi_run_ddl(sql).map_err(|e| error(sql, &e))
}
fn statement(
client: &SpiClient<'_>,
sql: &str,
args: &[DatumWithOid<'_>],
mutating: bool,
over: Option<Oid>,
) -> pgrx::spi::Result<std::rc::Rc<pgrx::spi::OwnedPreparedStatement>> {
let types: Vec<Oid> = args.iter().map(DatumWithOid::oid).collect();
let key = (sql.to_string(), types, mutating, over);
if let Some(kept) = crate::cache::STATEMENTS.with(|m| m.get(&key)) {
return Ok(kept);
}
let pg_types: Vec<pgrx::PgOid> = key.1.iter().map(|&t| pgrx::PgOid::from(t)).collect();
let prepared = if mutating {
client.prepare_mut(sql, &pg_types)?
} else {
client.prepare(sql, &pg_types)?
};
let kept = std::rc::Rc::new(prepared.keep());
crate::metrics::metrics_api::record_plan_prepared();
if let Some(table) = over {
crate::cache::watch(&[table]);
}
crate::cache::STATEMENTS.with(|m| m.insert(key, kept.clone()));
Ok(kept)
}
pub fn kept_select<'conn>(
client: &SpiClient<'conn>,
sql: &str,
args: &[DatumWithOid<'_>],
) -> pgrx::spi::Result<SpiTupleTable<'conn>> {
let kept = statement(client, sql, args, false, None)?;
client.select(&*kept, None, args)
}
pub fn kept_update<'conn>(
client: &mut SpiClient<'conn>,
sql: &str,
args: &[DatumWithOid<'_>],
) -> pgrx::spi::Result<SpiTupleTable<'conn>> {
let kept = statement(client, sql, args, true, None)?;
client.update(&*kept, None, args)
}
pub fn kept_update_over<'conn>(
client: &mut SpiClient<'conn>,
table: Oid,
sql: &str,
args: &[DatumWithOid<'_>],
) -> pgrx::spi::Result<SpiTupleTable<'conn>> {
let kept = statement(client, sql, args, true, Some(table))?;
client.update(&*kept, None, args)
}
pub type TextRow = Vec<Option<String>>;
pub fn latest_rows_connected(
sql: &str,
args: &[DatumWithOid<'_>],
keep: Option<Plan>,
) -> TViewResult<Vec<TextRow>> {
let (mut types, mut values, nulls) = bind(args);
let kept = match keep {
Some(plan) => Some(kept_plan(sql, &mut types, plan)?),
None => None,
};
unsafe {
let plan = if let Some(plan) = &kept {
plan.0.as_ptr()
} else {
let src = std::ffi::CString::new(sql).map_err(|e| error(sql, &e))?;
let nargs = i32::try_from(types.len()).map_err(|e| error(sql, &e))?;
let plan = pg_sys::SPI_prepare(src.as_ptr(), nargs, types.as_mut_ptr());
if plan.is_null() {
return Err(error(
sql,
&format!("SPI_prepare failed ({})", { pg_sys::SPI_result }),
));
}
plan
};
let rc = pg_sys::SPI_execute_snapshot(
plan,
values.as_mut_ptr(),
nulls.as_ptr(),
pg_sys::GetLatestSnapshot(),
std::ptr::null_mut(),
true,
false,
0,
);
let rows = tuptable_rows(sql, rc);
if kept.is_none() {
pg_sys::SPI_freeplan(plan);
}
rows
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Plan {
Generic,
PerExecution,
}
impl Plan {
#[must_use]
pub const fn and(self, other: Self) -> Self {
match (self, other) {
(Self::Generic, Self::Generic) => Self::Generic,
_ => Self::PerExecution,
}
}
const fn cursor_option(self) -> u32 {
match self {
Self::Generic => pg_sys::CURSOR_OPT_GENERIC_PLAN,
Self::PerExecution => pg_sys::CURSOR_OPT_CUSTOM_PLAN,
}
}
}
pub struct KeptPlan(std::ptr::NonNull<pg_sys::_SPI_plan>);
impl Drop for KeptPlan {
fn drop(&mut self) {
unsafe {
pg_sys::SPI_freeplan(self.0.as_ptr());
}
}
}
pub fn kept_rows(sql: &str, args: &[DatumWithOid<'_>], plan: Plan) -> TViewResult<Vec<TextRow>> {
kept_rows_as(sql, args, plan, |row| Ok(row.texts()))
}
pub fn kept_rows_as<R>(
sql: &str,
args: &[DatumWithOid<'_>],
plan: Plan,
read: impl FnMut(&KeptRow<'_>) -> TViewResult<R>,
) -> TViewResult<Vec<R>> {
let (mut types, mut values, nulls) = bind(args);
Spi::connect(|_| {
let plan = kept_plan(sql, &mut types, plan)?;
unsafe {
let rc = pg_sys::SPI_execute_plan(
plan.0.as_ptr(),
values.as_mut_ptr(),
nulls.as_ptr(),
false,
0,
);
tuptable_map(sql, rc, read)
}
})
}
fn kept_plan(sql: &str, types: &mut [Oid], mode: Plan) -> TViewResult<std::rc::Rc<KeptPlan>> {
let key = (mode, sql.to_string());
if let Some(plan) = crate::cache::PLANS.with(|m| m.get(&key)) {
return Ok(plan);
}
let src = std::ffi::CString::new(sql).map_err(|e| error(sql, &e))?;
let nargs = i32::try_from(types.len()).map_err(|e| error(sql, &e))?;
let plan = unsafe {
let plan = pg_sys::SPI_prepare_cursor(
src.as_ptr(),
nargs,
types.as_mut_ptr(),
mode.cursor_option().cast_signed(),
);
let plan = std::ptr::NonNull::new(plan).ok_or_else(|| {
error(
sql,
&format!("SPI_prepare failed ({})", { pg_sys::SPI_result }),
)
})?;
pg_sys::SPI_keepplan(plan.as_ptr());
std::rc::Rc::new(KeptPlan(plan))
};
crate::metrics::metrics_api::record_plan_prepared();
crate::cache::PLANS.with(|m| m.insert(key, plan.clone()));
Ok(plan)
}
fn bind(args: &[DatumWithOid<'_>]) -> (Vec<Oid>, Vec<pg_sys::Datum>, Vec<std::ffi::c_char>) {
let types = args.iter().map(DatumWithOid::oid).collect();
let values = args
.iter()
.map(|a| {
a.datum()
.map_or_else(|| pg_sys::Datum::from(0), pgrx::datum::Datum::sans_lifetime)
})
.collect();
let nulls = args
.iter()
.map(|a| if a.datum().is_some() { b' ' } else { b'n' }.cast_signed())
.collect();
(types, values, nulls)
}
pub struct KeptRow<'a> {
sql: &'a str,
tuple: pg_sys::HeapTuple,
tupdesc: pg_sys::TupleDesc,
}
pub trait OwnedColumn: FromDatum + IntoDatum + sealed::Sealed {}
impl OwnedColumn for i64 {}
impl OwnedColumn for i32 {}
impl OwnedColumn for bool {}
impl OwnedColumn for Oid {}
impl OwnedColumn for String {}
impl OwnedColumn for pgrx::JsonB {}
mod sealed {
pub trait Sealed {}
impl Sealed for i64 {}
impl Sealed for i32 {}
impl Sealed for bool {}
impl Sealed for super::Oid {}
impl Sealed for String {}
impl Sealed for pgrx::JsonB {}
}
impl KeptRow<'_> {
pub fn get<T: OwnedColumn>(&self, column: i32) -> TViewResult<Option<T>> {
unsafe {
let mut is_null = false;
let datum = pg_sys::SPI_getbinval(self.tuple, self.tupdesc, column, &raw mut is_null);
let type_oid = pg_sys::SPI_gettypeid(self.tupdesc, column);
T::try_from_datum(datum, is_null, type_oid).map_err(|e| error(self.sql, &e))
}
}
fn texts(&self) -> TextRow {
unsafe {
(1..=(*self.tupdesc).natts)
.map(|column| {
let value = pg_sys::SPI_getvalue(self.tuple, self.tupdesc, column);
if value.is_null() {
None
} else {
let text = std::ffi::CStr::from_ptr(value)
.to_string_lossy()
.into_owned();
pg_sys::pfree(value.cast());
Some(text)
}
})
.collect()
}
}
}
unsafe fn tuptable_rows(sql: &str, rc: i32) -> TViewResult<Vec<TextRow>> {
unsafe { tuptable_map(sql, rc, |row| Ok(row.texts())) }
}
unsafe fn tuptable_map<R>(
sql: &str,
rc: i32,
mut read: impl FnMut(&KeptRow<'_>) -> TViewResult<R>,
) -> TViewResult<Vec<R>> {
let returning = [
pg_sys::SPI_OK_SELECT,
pg_sys::SPI_OK_INSERT_RETURNING,
pg_sys::SPI_OK_UPDATE_RETURNING,
pg_sys::SPI_OK_DELETE_RETURNING,
];
if !returning.iter().any(|&ok| rc == ok.cast_signed()) {
return Err(error(sql, &format!("SPI returned {rc}")));
}
unsafe {
let table = pg_sys::SPI_tuptable;
let tupdesc = (*table).tupdesc;
let rows = (0..usize::try_from(pg_sys::SPI_processed).unwrap_or(0))
.map(|i| {
read(&KeptRow {
sql,
tuple: *(*table).vals.add(i),
tupdesc,
})
})
.collect();
pg_sys::SPI_freetuptable(table);
rows
}
}
#[cfg(test)]
mod tests {
use super::{LookupKeys, Plan};
#[test]
fn no_keys_is_no_lookup() {
assert!(LookupKeys::of(Vec::new()).is_none());
}
#[test]
fn one_key_is_compared_from_a_generic_plan() {
let keys = LookupKeys::of(vec![7]).expect("a key");
assert_eq!(
keys.predicate("t.\"fk_cat\"", 1),
"t.\"fk_cat\" = $1::pg_catalog.int8"
);
assert_eq!(keys.plan(), Plan::Generic);
}
#[test]
fn several_keys_are_an_array_planned_per_execution() {
let keys = LookupKeys::of(vec![7, 8]).expect("keys");
assert_eq!(
keys.predicate("\"fk_cat\"", 2),
"\"fk_cat\" = ANY ($2::pg_catalog.int8[])"
);
assert_eq!(keys.plan(), Plan::PerExecution);
}
}