use rupg_catalog::{FIRST_NORMAL_OID, Function, ObjRef, PG_PROC};
use rupg_common::{Error, Result, SqlState};
use rupg_sql::nodes::{
AlterFunctionStmt, AlterObjectSchemaStmt, AlterOwnerStmt, DefElem, Node, ObjectType,
ObjectWithArgs,
};
use super::comment::not_owner;
use super::drop_function::Candidate;
use super::{Definer, not_yet};
use crate::typename::{names, place};
pub(super) fn is_function_kind(kind: ObjectType) -> bool {
matches!(
kind,
ObjectType::OBJECT_FUNCTION
| ObjectType::OBJECT_PROCEDURE
| ObjectType::OBJECT_ROUTINE
| ObjectType::OBJECT_AGGREGATE
)
}
fn owner_word(kind: ObjectType) -> &'static str {
super::drop_function::routine_word(kind)
}
fn function_name(object: Option<&Node>) -> Result<&ObjectWithArgs> {
match object {
Some(Node::ObjectWithArgs(item)) => Ok(item),
_ => Err(Error::internal("a function name with arguments")),
}
}
fn positive_number(def: &DefElem, word: &str) -> Result<f64> {
let value = match &def.arg {
Some(Node::Integer(value)) => f64::from(*value),
Some(Node::Float(text)) => text.parse().unwrap_or(f64::NAN),
_ => {
return Err(Error::new(
SqlState::SYNTAX_ERROR,
format!("{word} requires a numeric value"),
));
}
};
if value.is_nan() || value <= 0.0 {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
format!("{word} must be positive"),
));
}
Ok(value)
}
pub(super) fn positive_cost(def: &DefElem) -> Result<f32> {
#[allow(clippy::cast_possible_truncation)]
Ok(positive_number(def, "COST")? as f32)
}
fn whole_number(def: &DefElem, word: &str) -> Result<u32> {
let value = positive_number(def, word)?;
if value.fract() != 0.0 || value > f64::from(u32::MAX) {
return Err(not_yet(&format!("a {word} that is not a whole number"), None));
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Ok(value as u32)
}
impl Definer<'_, '_> {
fn lookup_function(&mut self, kind: ObjectType, item: &ObjectWithArgs) -> Result<Candidate> {
self.find_function(kind, item, false)?
.ok_or_else(|| Error::internal("a function that the lookup did not find"))
}
fn check_function_owner(
&self,
kind: ObjectType,
item: &ObjectWithArgs,
found: &Candidate,
) -> Result<()> {
if !self.is_superuser() && found.owner != self.user {
return Err(not_owner(owner_word(kind), &names(&item.objname).join(".")));
}
Ok(())
}
fn altered_function(&mut self, kind: ObjectType, item: &ObjectWithArgs) -> Result<Candidate> {
let found = self.lookup_function(kind, item)?;
self.check_function_owner(kind, item, &found)?;
Ok(found)
}
fn check_user_function(found: &Candidate) -> Result<()> {
if found.oid < FIRST_NORMAL_OID {
return Err(not_yet("ALTER FUNCTION of a built-in function", None));
}
Ok(())
}
pub(super) fn alter_function(&mut self, stmt: &AlterFunctionStmt) -> Result<()> {
let item = stmt.func.as_deref().ok_or_else(|| Error::internal("a function name"))?;
let found = self.altered_function(stmt.objtype, item)?;
if found.kind == b'a' {
return Err(Error::new(
SqlState::WRONG_OBJECT_TYPE,
format!("\"{}\" is an aggregate function", names(&item.objname).join(".")),
));
}
let mut seen: Vec<&str> = Vec::new();
let mut defs: Vec<&DefElem> = Vec::new();
for def in stmt.actions.iter().flatten() {
let Node::DefElem(def) = def else { continue };
let name = def.defname.as_deref().unwrap_or_default();
if name != "set" {
if seen.contains(&name) {
return Err(Error::new(
SqlState::SYNTAX_ERROR,
"conflicting or redundant options",
)
.with_position(place(def.location)));
}
seen.push(name);
}
defs.push(def);
}
Self::check_user_function(&found)?;
let function = self.catalog.function(found.oid).ok_or_else(|| {
Error::internal(format!("cache lookup failed for function {}", found.oid))
})?;
let mut new = function.clone();
for def in defs {
let name = def.defname.as_deref().unwrap_or_default();
let text = match &def.arg {
Some(Node::String(text)) => Some(&**text),
_ => None,
};
match (name, text) {
("strict", _) => new.strict = matches!(def.arg, Some(Node::Boolean(true))),
("leakproof", _) => {
new.leakproof = matches!(def.arg, Some(Node::Boolean(true)));
if new.leakproof && !self.is_superuser() {
return Err(super::function::only_superuser_leakproof());
}
}
("volatility", Some("immutable")) => new.volatile = b'i',
("volatility", Some("stable")) => new.volatile = b's',
("volatility", Some("volatile")) => new.volatile = b'v',
("parallel", Some("safe")) => new.parallel = b's',
("parallel", Some("restricted")) => new.parallel = b'r',
("parallel", Some("unsafe")) => new.parallel = b'u',
("parallel", _) => {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
"parameter \"parallel\" must be SAFE, RESTRICTED, or UNSAFE",
));
}
("set", _) => match &def.arg {
Some(Node::VariableSetStmt(set)) => {
super::function::update_config(self.an.env, &mut new.config, set)?;
}
_ => return Err(Error::internal("a SET of ALTER FUNCTION with no setting")),
},
("security", _) => new.secdef = matches!(def.arg, Some(Node::Boolean(true))),
("cost", _) => new.cost = positive_cost(def)?,
("rows", _) => {
new.rows = whole_number(def, "ROWS")?;
if !new.retset {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
"ROWS is not applicable when function does not return a set",
));
}
}
_ => {
return Err(not_yet(
&format!("the option {name} of ALTER FUNCTION"),
place(def.location),
));
}
}
}
self.catalog.alter_function(found.oid, |f| *f = new)
}
pub(super) fn rename_function(
&mut self,
kind: ObjectType,
object: Option<&Node>,
newname: &str,
) -> Result<()> {
let item = function_name(object)?;
let found = self.lookup_function(kind, item)?;
self.check_function_owner(ObjectType::OBJECT_FUNCTION, item, &found)?;
let function = self.user_function(&found)?;
if !self.is_superuser() {
self.check_create_in(function.namespace, None)?;
}
self.check_function_in(function.namespace, newname, &function.argtypes)?;
self.check_not_called(found.oid, "RENAME")?;
self.catalog.alter_function(found.oid, |f| f.name = newname.to_string())?;
self.views_using(|object| object.class == PG_PROC && object.oid == found.oid)
}
pub(super) fn alter_function_schema(&mut self, stmt: &AlterObjectSchemaStmt) -> Result<()> {
let item = function_name(stmt.object.as_ref())?;
let found = self.lookup_function(stmt.objectType, item)?;
let schema = stmt.newschema.as_deref().unwrap_or_default();
let namespace = self.an.schema(schema).ok_or_else(|| {
Error::new(SqlState::UNDEFINED_SCHEMA, format!("schema \"{schema}\" does not exist"))
})?;
self.check_create_in(namespace, None)?;
let function = self.user_function(&found)?;
if function.namespace == namespace {
return Ok(());
}
self.check_function_owner(ObjectType::OBJECT_FUNCTION, item, &found)?;
self.check_function_in(namespace, &function.name, &function.argtypes)?;
self.check_not_called(found.oid, "SET SCHEMA")?;
self.catalog.alter_function(found.oid, |f| f.namespace = namespace)?;
self.views_using(|object| object.class == PG_PROC && object.oid == found.oid)
}
pub(super) fn alter_function_owner(&mut self, stmt: &AlterOwnerStmt) -> Result<()> {
let role = stmt.newowner.as_deref().cloned().unwrap_or_default();
let new = self.role_oid(&role)?;
let item = function_name(stmt.object.as_ref())?;
let found = self.lookup_function(stmt.objectType, item)?;
if found.owner == new {
return Ok(());
}
if !self.is_superuser() {
self.check_function_owner(ObjectType::OBJECT_FUNCTION, item, &found)?;
self.check_can_set_role(new)?;
let namespace = self.catalog.function(found.oid).map_or(0, |f| f.namespace);
if !self.may_create_in(namespace, new) {
return Err(Error::new(
SqlState::INSUFFICIENT_PRIVILEGE,
format!("permission denied for schema {}", self.schema_name(namespace)),
));
}
}
Self::check_user_function(&found)?;
let roles = rupg_pgcatalog::roles::current();
let (old, new_name) = (roles.name(found.owner).unwrap_or_default(), roles.name(new));
let object = ObjRef::new(PG_PROC, found.oid);
self.catalog.set_object_owner(object, new, old, new_name.unwrap_or_default())
}
fn user_function(&self, found: &Candidate) -> Result<Function> {
Self::check_user_function(found)?;
self.catalog.function(found.oid).cloned().ok_or_else(|| {
Error::internal(format!("cache lookup failed for function {}", found.oid))
})
}
fn check_function_in(&self, namespace: u32, name: &str, argtypes: &[u32]) -> Result<()> {
if self.functions_in(namespace, name).iter().any(|c| c.argtypes == argtypes) {
let args: Vec<String> = argtypes.iter().map(|&ty| self.type_text(ty)).collect();
return Err(Error::new(
SqlState::DUPLICATE_FUNCTION,
format!(
"function {name}({}) already exists in schema \"{}\"",
args.join(", "),
self.schema_name(namespace)
),
));
}
Ok(())
}
fn check_not_called(&self, oid: u32, what: &str) -> Result<()> {
let me = ObjRef::new(PG_PROC, oid);
if self.catalog.depends().iter().any(|d| {
d.referenced == me
&& d.object.class == PG_PROC
&& self.catalog.function(d.object.oid).is_none_or(|f| f.kind != b'a')
}) {
return Err(not_yet(
&format!("ALTER FUNCTION ... {what} of a function that another function calls"),
None,
));
}
Ok(())
}
}