use rupg_catalog::RelKind;
use rupg_common::{Error, Result, SqlState};
use rupg_pgcatalog::builtin::{self, Named};
use rupg_sql::nodes::{LockStmt, Node, RangeVar};
use super::relkind_detail;
use super::view::parse_bool;
use crate::expr::{Expr, ExprKind};
use crate::{Analyzer, Env, Params, Query};
pub type Allowed<'a> = dyn FnMut(u32, u32) -> Result<bool> + 'a;
pub type Take<'a> = dyn FnMut(u32) -> Result<bool> + 'a;
pub fn lock_table(
stmt: &LockStmt,
env: &dyn Env,
user: u32,
allowed: &mut Allowed<'_>,
take: &mut Take<'_>,
) -> Result<()> {
let params = Params::default();
let an = Analyzer::new(env, ¶ms);
let mut locker = Locker { an, allowed, take, ancestors: Vec::new() };
for node in stmt.relations.iter().flatten() {
let Node::RangeVar(rv) = node else { continue };
let relid = lookup(&locker.an, rv)?;
let name = rv.relname.as_deref().unwrap_or_default();
let kind = relkind(env, relid);
if !matches!(kind, b'r' | b'p' | b'v') {
return Err(Error::new(
SqlState::WRONG_OBJECT_TYPE,
format!("cannot lock relation \"{name}\""),
)
.with_detail(relkind_detail(kind)));
}
if !(locker.allowed)(relid, user)? {
return Err(denied(kind, name));
}
if !(locker.take)(relid)? {
let name = match rv.schemaname.as_deref() {
Some(schema) => format!("{schema}.{name}"),
None => name.to_owned(),
};
return Err(not_free(&name));
}
if kind == b'v' {
locker.view(relid, user)?;
} else if rv.inh {
locker.children(relid)?;
}
}
Ok(())
}
pub(super) fn lookup(an: &Analyzer<'_>, rv: &RangeVar) -> Result<u32> {
let found = an.lookup_relation(rv).map_err(|error| error.with_position(None))?;
if let Some(relid) = found {
return Ok(relid);
}
let name = rv.relname.as_deref().unwrap_or_default();
Err(match rv.schemaname.as_deref() {
Some(schema) if an.schema(schema).is_none() => {
Error::new(SqlState::UNDEFINED_SCHEMA, format!("schema \"{schema}\" does not exist"))
}
Some(schema) => Error::new(
SqlState::UNDEFINED_TABLE,
format!("relation \"{schema}.{name}\" does not exist"),
),
None => {
Error::new(SqlState::UNDEFINED_TABLE, format!("relation \"{name}\" does not exist"))
}
})
}
struct Locker<'a, 'f> {
an: Analyzer<'a>,
allowed: &'f mut Allowed<'f>,
take: &'f mut Take<'f>,
ancestors: Vec<u32>,
}
impl Locker<'_, '_> {
fn children(&mut self, relid: u32) -> Result<()> {
let children = self.an.env.catalog().map(|c| c.all_inheritors(relid)).unwrap_or_default();
for child in children {
if !(self.take)(child)? {
return Err(not_free(&class_name(self.an.env, child)));
}
}
Ok(())
}
fn view(&mut self, relid: u32, user: u32) -> Result<()> {
let Some(query) = self.an.view_query(relid)? else { return Ok(()) };
self.view_query(relid, &query, user)
}
fn view_query(&mut self, relid: u32, query: &Query, user: u32) -> Result<()> {
let rel = self.an.env.catalog().and_then(|c| c.relation(relid));
let invoker = rel.is_some_and(|r| {
r.options
.iter()
.filter_map(|o| o.strip_prefix("security_invoker="))
.next_back()
.and_then(parse_bool)
.unwrap_or(false)
});
let check_as = match rel {
Some(rel) if !invoker => rel.owner,
_ => user,
};
self.ancestors.push(relid);
let result = self.walk(query, check_as, user);
self.ancestors.pop();
result
}
fn walk(&mut self, query: &Query, check_as: u32, user: u32) -> Result<()> {
for relation in &query.relations {
if relation.oid == 0 || relation.cte.is_some() {
continue;
}
let kind = relkind(self.an.env, relation.oid);
if !matches!(kind, b'r' | b'p' | b'v') || self.ancestors.contains(&relation.oid) {
continue;
}
if !(self.allowed)(relation.oid, check_as)? {
return Err(denied(kind, &class_name(self.an.env, relation.oid)));
}
if !(self.take)(relation.oid)? {
return Err(not_free(&class_name(self.an.env, relation.oid)));
}
if kind == b'v' {
if let Some(sub) = relation.subquery.as_deref() {
self.view_query(relation.oid, sub, user)?;
}
} else if !relation.only {
self.children(relation.oid)?;
}
}
for expr in query.exprs() {
self.walk_expr(expr, check_as, user)?;
}
for cte in &query.ctes {
self.walk(&cte.query, check_as, user)?;
}
for relation in &query.relations {
if relation.oid == 0
&& relation.cte.is_none()
&& let Some(sub) = relation.subquery.as_deref()
{
self.walk(sub, check_as, user)?;
}
}
Ok(())
}
fn walk_expr(&mut self, expr: &Expr, check_as: u32, user: u32) -> Result<()> {
if let ExprKind::SubLink(sub) = &expr.kind {
self.walk(&sub.query, check_as, user)?;
}
for child in expr.children() {
self.walk_expr(child, check_as, user)?;
}
Ok(())
}
}
pub(crate) fn relkind(env: &dyn Env, relid: u32) -> u8 {
match env.catalog().and_then(|c| c.relation(relid)) {
Some(rel) => match rel.kind {
RelKind::Table => b'r',
RelKind::Index => b'i',
RelKind::Sequence => b'S',
RelKind::Toast => b't',
RelKind::View => b'v',
RelKind::Composite => b'c',
RelKind::Partitioned => b'p',
RelKind::PartitionedIndex => b'I',
RelKind::MatView => b'm',
},
None => builtin::class_by_oid(relid).map_or(0, |row| row.kind),
}
}
pub(super) fn class_name(env: &dyn Env, relid: u32) -> String {
match env.catalog().and_then(|c| c.relation(relid)) {
Some(rel) => rel.name.clone(),
None => builtin::named_by_oid(Named::Class, relid)
.map_or_else(String::new, |n| n.name.to_string()),
}
}
fn denied(kind: u8, name: &str) -> Error {
let object = if kind == b'v' { "view" } else { "table" };
Error::new(SqlState::INSUFFICIENT_PRIVILEGE, format!("permission denied for {object} {name}"))
}
fn not_free(name: &str) -> Error {
Error::new(
SqlState::LOCK_NOT_AVAILABLE,
format!("could not obtain lock on relation \"{name}\""),
)
}