use std::collections::BTreeSet;
use rupg_analyze::{ExprKind, FromItem, Query, Target};
use rupg_common::Result;
use rupg_pgcatalog::builtin;
use rupg_types::Value;
use crate::reg;
use crate::{Call, Kernel, Session, bad_value};
const UPDATE: i32 = 1 << 2;
const INSERT: i32 = 1 << 3;
const DELETE: i32 = 1 << 4;
const ALL_EVENTS: i32 = INSERT | UPDATE | DELETE;
fn relkind(oid: u32, session: &dyn Session) -> Option<char> {
session
.catalog()
.and_then(|c| c.relation(oid))
.map(|r| r.kind.code())
.or_else(|| builtin::class_by_oid(oid).map(|r| char::from(r.kind)))
}
fn base_column(target: &Target, base: usize) -> Option<i16> {
if target.junk {
return None;
}
match &target.expr.kind {
ExprKind::Var(var) if var.relation == base && var.levels_up == 0 && var.attnum > 0 => {
Some(var.attnum)
}
_ => None,
}
}
fn auto_base(query: &Query, session: &dyn Session) -> Option<(usize, u32)> {
if !query.distinct.is_empty()
|| query.grouped
|| query.set_op.is_some()
|| query.limit.is_some()
|| query.offset.is_some()
|| query.targets.iter().any(|t| t.expr.first_window().is_some())
|| query.has_target_srfs()
{
return None;
}
let [FromItem::Relation(index)] = query.from.as_slice() else { return None };
let relation = &query.relations[*index];
if relation.oid == 0 || relation.function.is_some() || relation.values.is_some() {
return None;
}
let kind = relkind(relation.oid, session)?;
matches!(kind, 'r' | 'f' | 'v' | 'p').then_some((*index, relation.oid))
}
fn relation_is_updatable(
oid: u32,
outer: &mut Vec<u32>,
columns: Option<&BTreeSet<i16>>,
session: &dyn Session,
) -> Result<i32> {
let Some(kind) = relkind(oid, session) else { return Ok(0) };
if outer.contains(&oid) {
return Ok(0);
}
match kind {
'r' | 'p' => return Ok(ALL_EVENTS),
'v' => {}
_ => return Ok(0),
}
let rules = session.catalog().into_iter().flat_map(|c| c.rules()).filter(|r| r.relation == oid);
let rule_events = rules
.filter(|r| r.instead && !r.qualified)
.fold(0, |events, r| events | ((1 << (r.event - b'0')) & ALL_EVENTS));
if rule_events == ALL_EVENTS {
return Ok(rule_events);
}
let env = reg::SessionEnv(session);
let Some(query) = rupg_analyze::view_query(&env, oid)? else { return Ok(rule_events) };
let Some((base, base_oid)) = auto_base(&query, session) else { return Ok(rule_events) };
let updatable: Vec<i16> = (1i16..)
.zip(&query.targets)
.filter(|(n, _)| columns.is_none_or(|c| c.contains(n)))
.filter_map(|(_, target)| base_column(target, base))
.collect();
let mut events = if updatable.is_empty() { DELETE } else { ALL_EVENTS };
if !matches!(relkind(base_oid, session), Some('r' | 'p')) {
let base_columns: BTreeSet<i16> = updatable.into_iter().collect();
let base_columns = (!base_columns.is_empty()).then_some(&base_columns);
outer.push(oid);
events &= relation_is_updatable(base_oid, outer, base_columns, session)?;
outer.pop();
}
Ok(rule_events | events)
}
fn pg_relation_is_updatable(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
Ok(Value::Int4(relation_is_updatable(oid, &mut Vec::new(), None, call.session)?))
}
fn pg_column_is_updatable(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
let Some(Value::Int2(attnum)) = args.get(1) else { return Err(bad_value()) };
if *attnum <= 0 {
return Ok(Value::Bool(false));
}
let columns = BTreeSet::from([*attnum]);
let events = relation_is_updatable(oid, &mut Vec::new(), Some(&columns), call.session)?;
Ok(Value::Bool(events & (UPDATE | DELETE) == UPDATE | DELETE))
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
let kernel: Kernel = match src {
"pg_relation_is_updatable" => pg_relation_is_updatable,
"pg_column_is_updatable" => pg_column_is_updatable,
_ => return None,
};
Some(kernel)
}