rupg-func 0.1.84

The 3,414 built-in functions, the 805 operators, the 249 casts, the aggregates and window functions, the regular expression engine.
Documentation
//! The functions of the cumulative statistics for one object, from `pgstatfuncs.c`: the counters of a relation or an index, of a function and of a database, and the `xact` forms that read the counts of the transaction.
//!
//! rupg keeps the sequential scans of a relation, their count `numscans` and the time of the last one `lastscan`, and the counters of the rows that the transactions wrote: `tuples_inserted`, `tuples_updated`, `tuples_deleted`, `live_tuples`, `dead_tuples`, `mod_since_analyze` and `ins_since_vacuum`. The other functions give the value that PostgreSQL gives for an object that has no entry in the statistics: 0 for a counter and for a total time of a relation or a database, and null for a time stamp and for each value of a function. The views `pg_stat_*` and `pg_statio_*` then have a row for each object, with the columns and the types of PostgreSQL (spec/07 section 7.14).

use rupg_common::Result;
use rupg_types::Value;

use crate::{Call, Kernel, bad_value};

/// The counters of a relation that `PG_STAT_GET_RELENTRY_INT64` and `PG_STAT_GET_XACT_RELENTRY_INT64` read.
const RELATION_COUNTERS: [&str; 18] = [
    "analyze_count",
    "autoanalyze_count",
    "autovacuum_count",
    "blocks_fetched",
    "blocks_hit",
    "dead_tuples",
    "ins_since_vacuum",
    "live_tuples",
    "mod_since_analyze",
    "numscans",
    "tuples_deleted",
    "tuples_fetched",
    "tuples_hot_updated",
    "tuples_newpage_updated",
    "tuples_inserted",
    "tuples_returned",
    "tuples_updated",
    "vacuum_count",
];

/// The counters of a database that `PG_STAT_GET_DBENTRY_INT64` reads, after `pg_stat_get_db_`.
const DATABASE_COUNTERS: [&str; 25] = [
    "blocks_fetched",
    "blocks_hit",
    "conflict_bufferpin",
    "conflict_lock",
    "conflict_snapshot",
    "conflict_startup_deadlock",
    "conflict_tablespace",
    "conflict_logicalslot",
    "conflict_all",
    "deadlocks",
    "sessions",
    "sessions_abandoned",
    "sessions_fatal",
    "sessions_killed",
    "parallel_workers_to_launch",
    "parallel_workers_launched",
    "temp_bytes",
    "temp_files",
    "tuples_deleted",
    "tuples_fetched",
    "tuples_inserted",
    "tuples_returned",
    "tuples_updated",
    "xact_commit",
    "xact_rollback",
];

/// The times of a database in milliseconds that `PG_STAT_GET_DBENTRY_FLOAT8_MS` reads, after `pg_stat_get_db_`.
const DATABASE_TIMES: [&str; 5] =
    ["active_time", "blk_read_time", "blk_write_time", "idle_in_transaction_time", "session_time"];

/// The total times of a relation that `PG_STAT_GET_RELENTRY_FLOAT8` reads.
const RELATION_TIMES: [&str; 4] =
    ["total_vacuum_time", "total_autovacuum_time", "total_analyze_time", "total_autoanalyze_time"];

/// The time stamps of a relation that `PG_STAT_GET_RELENTRY_TIMESTAMPTZ` reads.
const RELATION_STAMPS: [&str; 6] = [
    "last_analyze_time",
    "last_autoanalyze_time",
    "last_autovacuum_time",
    "last_vacuum_time",
    "lastscan",
    "stat_reset_time",
];

/// The counters of a relation in the statistics of the server, the part of `PgStat_StatTabEntry` that rupg keeps.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RelationCounts {
    /// `numscans`: the count of the sequential scans of the relation.
    pub numscans: i64,
    /// `lastscan`: the end of the last transaction that scanned the relation, in microseconds since 2000-01-01 UTC, or `None` before the first scan.
    pub lastscan: Option<i64>,
    /// `tuples_inserted`, `tuples_updated` and `tuples_deleted`: the rows that the transactions wrote, also the transactions that rolled back.
    pub written: Writes,
    /// `live_tuples`: the rows that the committed transactions inserted and did not delete.
    pub live_tuples: i64,
    /// `dead_tuples`: the old versions of the rows that the committed updates and deletes left, and the rows that the rolled back transactions inserted or updated.
    pub dead_tuples: i64,
    /// `mod_since_analyze`: the rows that the committed transactions wrote.
    pub mod_since_analyze: i64,
    /// `ins_since_vacuum`: the rows that the transactions inserted.
    pub ins_since_vacuum: i64,
}

/// The rows that a transaction inserted, updated and deleted in one relation, as `pgstat_count_heap_insert`, `pgstat_count_heap_update` and `pgstat_count_heap_delete` count them.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Writes {
    /// The rows that the transaction inserted.
    pub inserted: i64,
    /// The rows that the transaction updated.
    pub updated: i64,
    /// The rows that the transaction deleted.
    pub deleted: i64,
}

impl std::ops::Add for Writes {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            inserted: self.inserted + other.inserted,
            updated: self.updated + other.updated,
            deleted: self.deleted + other.deleted,
        }
    }
}

impl std::ops::Sub for Writes {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self {
            inserted: self.inserted - other.inserted,
            updated: self.updated - other.updated,
            deleted: self.deleted - other.deleted,
        }
    }
}

/// The OID of the object. A strict function does not get null, and PostgreSQL does not check that the object exists.
fn object(args: &[Value]) -> Result<u32> {
    args.first().and_then(Value::as_oid).ok_or_else(bad_value)
}

/// A counter of an object with no entry.
fn counter(_: &Call<'_>, args: &[Value]) -> Result<Value> {
    object(args)?;
    Ok(Value::Int8(0))
}

/// `pg_stat_get_numscans`: the sequential scans of a relation that the server reported.
fn numscans(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    let counts = call.session.relation_counts(object(args)?);
    Ok(Value::Int8(counts.map_or(0, |c| c.numscans)))
}

/// `pg_stat_get_lastscan`: the time of the last sequential scan of a relation, or null.
fn lastscan(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    let counts = call.session.relation_counts(object(args)?);
    Ok(counts.and_then(|c| c.lastscan).map_or(Value::Null, Value::TimestampTz))
}

/// `pg_stat_get_xact_numscans`: the sequential scans of a relation that the transaction counted.
fn xact_numscans(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    Ok(Value::Int8(call.session.pending_scans(object(args)?)))
}

/// A counter of a relation that the server reported, or 0 for a relation with no entry.
fn relation_counter(
    call: &Call<'_>,
    args: &[Value],
    read: fn(&RelationCounts) -> i64,
) -> Result<Value> {
    let counts = call.session.relation_counts(object(args)?);
    Ok(Value::Int8(counts.as_ref().map_or(0, read)))
}

/// A count of the rows that the transaction wrote in a relation.
fn xact_counter(call: &Call<'_>, args: &[Value], read: fn(&Writes) -> i64) -> Result<Value> {
    Ok(Value::Int8(read(&call.session.pending_writes(object(args)?))))
}

/// `pg_stat_get_tuples_inserted`.
fn tuples_inserted(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.written.inserted)
}

/// `pg_stat_get_tuples_updated`.
fn tuples_updated(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.written.updated)
}

/// `pg_stat_get_tuples_deleted`.
fn tuples_deleted(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.written.deleted)
}

/// `pg_stat_get_live_tuples`.
fn live_tuples(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.live_tuples)
}

/// `pg_stat_get_dead_tuples`.
fn dead_tuples(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.dead_tuples)
}

/// `pg_stat_get_mod_since_analyze`.
fn mod_since_analyze(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.mod_since_analyze)
}

/// `pg_stat_get_ins_since_vacuum`.
fn ins_since_vacuum(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    relation_counter(call, args, |c| c.ins_since_vacuum)
}

/// `pg_stat_get_xact_tuples_inserted`.
fn xact_inserted(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    xact_counter(call, args, |w| w.inserted)
}

/// `pg_stat_get_xact_tuples_updated`.
fn xact_updated(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    xact_counter(call, args, |w| w.updated)
}

/// `pg_stat_get_xact_tuples_deleted`.
fn xact_deleted(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    xact_counter(call, args, |w| w.deleted)
}

/// A total time of an object with no entry.
fn total_time(_: &Call<'_>, args: &[Value]) -> Result<Value> {
    object(args)?;
    Ok(Value::Float8(0.0))
}

/// A time stamp of an object with no entry, or a value of a function with no entry.
fn none(_: &Call<'_>, args: &[Value]) -> Result<Value> {
    object(args)?;
    Ok(Value::Null)
}

/// `pg_stat_get_db_checksum_failures`: null when the data has no checksums, as `DataChecksumsEnabled` tells, else the count of a database with no entry.
fn checksum_failures(call: &Call<'_>, args: &[Value]) -> Result<Value> {
    object(args)?;
    Ok(match call.session.setting("data_checksums").as_deref() {
        Some("on") => Value::Int8(0),
        _ => Value::Null,
    })
}

/// `pg_stat_force_next_flush`: the next end of a transaction reports the counts at once. rupg reports them at the end of each transaction, so the call has no effect. The result is `void`, whose value the engine keeps as an empty text.
fn force_next_flush(_: &Call<'_>, _: &[Value]) -> Result<Value> {
    Ok(Value::text(""))
}

/// The kernel of a function of this module by its `prosrc`.
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
    if src == "pg_stat_force_next_flush" {
        return Some(force_next_flush);
    }
    let name = src.strip_prefix("pg_stat_get_")?;
    if let Some(name) = name.strip_prefix("db_") {
        return Some(match name {
            "checksum_failures" => checksum_failures,
            "checksum_last_failure" | "stat_reset_time" => none,
            _ if DATABASE_COUNTERS.contains(&name) => counter,
            _ if DATABASE_TIMES.contains(&name) => total_time,
            _ => return None,
        });
    }
    if let Some(name) = name.strip_prefix("xact_") {
        return Some(match name {
            "function_calls" | "function_total_time" | "function_self_time" => none,
            "numscans" => xact_numscans,
            "tuples_inserted" => xact_inserted,
            "tuples_updated" => xact_updated,
            "tuples_deleted" => xact_deleted,
            _ if RELATION_COUNTERS.contains(&name) => counter,
            _ => return None,
        });
    }
    Some(match name {
        "function_calls" | "function_total_time" | "function_self_time" => none,
        "function_stat_reset_time" => none,
        "numscans" => numscans,
        "lastscan" => lastscan,
        "tuples_inserted" => tuples_inserted,
        "tuples_updated" => tuples_updated,
        "tuples_deleted" => tuples_deleted,
        "live_tuples" => live_tuples,
        "dead_tuples" => dead_tuples,
        "mod_since_analyze" => mod_since_analyze,
        "ins_since_vacuum" => ins_since_vacuum,
        _ if RELATION_COUNTERS.contains(&name) => counter,
        _ if RELATION_TIMES.contains(&name) => total_time,
        _ if RELATION_STAMPS.contains(&name) => none,
        _ => return None,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The module has the 78 functions of one object of `pg_proc`, each with one argument of the type `oid` and a result of the type `int8`, `float8` or `timestamptz`.
    #[test]
    fn covers_the_functions_of_one_object() {
        let mut found = 0;
        for proc in rupg_pgcatalog::builtin::procs() {
            if !proc.src.starts_with("pg_stat_get_") || by_src(proc.src).is_none() {
                continue;
            }
            found += 1;
            assert_eq!(proc.argtypes, [26], "{}", proc.src);
            assert!(matches!(proc.rettype, 20 | 701 | 1184), "{}", proc.src);
        }
        assert_eq!(found, 78);
    }
}