use rupg_catalog::{DepKind, ObjRef, RelKind};
use rupg_common::{Error, Result, SqlState};
use rupg_pgcatalog::builtin::{self, Named};
use rupg_types::{Record, Value, oid, qualified_name_list};
use crate::reg;
use crate::{Call, Kernel, bad_value, type_error};
const PG_CATALOG: u32 = 11;
const PG_CLASS: u32 = 1259;
fn oid_arg(args: &[Value], index: usize) -> Result<u32> {
args.get(index).and_then(Value::as_oid).ok_or_else(bad_value)
}
fn named_visible(kind: Named, call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
if matches!(
kind,
Named::Collation | Named::Config | Named::Dictionary | Named::Parser | Named::Template
) {
return Ok(reg::catalog_visible(kind, oid, call.session).map_or(Value::Null, Value::Bool));
}
Ok(builtin::named_by_oid(kind, oid)
.map_or(Value::Null, |row| Value::Bool(reg::visible(kind, row, call.session))))
}
fn table_visible(call: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(reg::class_visible(oid_arg(args, 0)?, call.session).map_or(Value::Null, Value::Bool))
}
fn type_visible(call: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(reg::type_visible(oid_arg(args, 0)?, call.session).map_or(Value::Null, Value::Bool))
}
fn function_visible(call: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(builtin::proc_by_oid(oid_arg(args, 0)?)
.map_or(Value::Null, |proc| Value::Bool(reg::function_visible(proc, call.session))))
}
fn operator_visible(call: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(builtin::operator_by_oid(oid_arg(args, 0)?)
.map_or(Value::Null, |op| Value::Bool(reg::operator_visible(op, call.session))))
}
fn statistics_obj_visible(call: &Call<'_>, args: &[Value]) -> Result<Value> {
Ok(reg::stat_ext_visible(oid_arg(args, 0)?, call.session).map_or(Value::Null, Value::Bool))
}
fn get_userbyid(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
Ok(Value::Text(match rupg_pgcatalog::roles::current().name(oid) {
Some(name) => name.to_string(),
None => format!("unknown (OID={oid})"),
}))
}
fn catalog_oid(args: &[Value]) -> Result<Option<u32>> {
let name = args.get(1).and_then(Value::as_str).ok_or_else(bad_value)?;
let rows = builtin::named(Named::Class);
Ok(rows.iter().find(|r| r.namespace == PG_CATALOG && r.name == name).map(|r| r.oid))
}
fn comment(text: Option<&str>) -> Value {
text.map_or(Value::Null, Value::text)
}
fn description(call: &Call<'_>, object: ObjRef) -> Value {
let user = call.session.catalog().and_then(|c| c.description(object));
comment(user.or_else(|| builtin::description(object.oid, object.class, object.sub)))
}
fn obj_description(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
Ok(match catalog_oid(args)? {
Some(class) => description(call, ObjRef::new(class, oid)),
None => Value::Null,
})
}
fn obj_description_any(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
let found = builtin::descriptions().iter().find(|d| d.objoid == oid && d.objsubid == 0);
let user = || {
let mut rows = call.session.catalog()?.descriptions();
rows.find(|(object, _)| object.oid == oid && object.sub == 0).map(|(_, text)| text)
};
Ok(comment(found.map(|d| d.description).or_else(user)))
}
fn col_description(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
let column = args
.get(1)
.and_then(Value::as_i64)
.and_then(|n| i32::try_from(n).ok())
.ok_or_else(bad_value)?;
Ok(description(call, ObjRef { class: PG_CLASS, oid, sub: column }))
}
fn shobj_description(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
Ok(comment(catalog_oid(args)?.and_then(|class| builtin::shared_description(oid, class))))
}
fn is_other_temp_schema(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
let temp = reg::namespace_name(oid, call.session)
.is_some_and(|name| name.starts_with("pg_temp_") || name.starts_with("pg_toast_temp_"));
Ok(Value::Bool(temp && !reg::is_own_temp(oid, call.session)))
}
fn get_sequence_data(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let relid = oid_arg(args, 0)?;
let sequence = call
.session
.catalog()
.and_then(|c| c.relation(relid))
.filter(|r| r.kind == RelKind::Sequence)
.and_then(|r| r.sequence.as_ref());
let values = match sequence {
Some(info) if crate::acl::sequence_readable(relid, call.session)? => {
let row = crate::sequence::row(info, call.session);
vec![Value::Int8(row.last_value), Value::Bool(row.is_called), Value::Int8(0)]
}
_ => vec![Value::Null; 3],
};
Ok(Value::Record(Box::new(Record::new(vec![oid::INT8, oid::BOOL, oid::PG_LSN], values))))
}
fn partition_root(_: &Call<'_>, _: &[Value]) -> Result<Value> {
Ok(Value::Null)
}
pub(crate) fn partition_members(_: &Call<'_>, _: &[Value]) -> Result<Vec<Vec<Value>>> {
Ok(Vec::new())
}
fn relation_is_publishable(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let relid = oid_arg(args, 0)?;
if reg::class_name(relid, call.session).is_none() {
return Ok(Value::Null);
}
let kind = call.session.catalog().and_then(|c| c.relation(relid)).map(|r| r.kind);
Ok(Value::Bool(matches!(kind, Some(RelKind::Table | RelKind::Partitioned | RelKind::Sequence))))
}
fn serial_sequence(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let (Some(table), Some(column)) = (args[0].as_str(), args[1].as_str()) else {
return Err(bad_value());
};
let names = qualified_name_list(table).map_err(type_error)?;
let relid = reg::relation_oid(&names, call.session)?;
let Some(attnum) = crate::acl::attnum(relid, column, call.session) else {
let relname = names.last().map_or("", |n| &**n);
return Err(Error::new(
SqlState::UNDEFINED_COLUMN,
format!("column \"{column}\" of relation \"{relname}\" does not exist"),
));
};
let Some(catalog) = call.session.catalog() else { return Ok(Value::Null) };
let column = ObjRef::column(relid, attnum);
let sequence = catalog.depends().iter().find(|d| {
d.referenced == column
&& d.object.class == PG_CLASS
&& d.object.sub == 0
&& matches!(d.kind, DepKind::Auto | DepKind::Internal)
&& catalog.relation(d.object.oid).is_some_and(|r| r.kind == RelKind::Sequence)
});
let Some(sequence) = sequence else { return Ok(Value::Null) };
let Some((name, namespace)) = reg::class_name(sequence.object.oid, call.session) else {
return Ok(Value::Null);
};
Ok(Value::Text(reg::qualified(reg::namespace_name(namespace, call.session), name)))
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
let kernel: Kernel = match src {
"pg_table_is_visible" => table_visible,
"pg_collation_is_visible" => |c, a| named_visible(Named::Collation, c, a),
"pg_conversion_is_visible" => |c, a| named_visible(Named::Conversion, c, a),
"pg_opclass_is_visible" => |c, a| named_visible(Named::Opclass, c, a),
"pg_opfamily_is_visible" => |c, a| named_visible(Named::Opfamily, c, a),
"pg_ts_config_is_visible" => |c, a| named_visible(Named::Config, c, a),
"pg_ts_dict_is_visible" => |c, a| named_visible(Named::Dictionary, c, a),
"pg_ts_parser_is_visible" => |c, a| named_visible(Named::Parser, c, a),
"pg_ts_template_is_visible" => |c, a| named_visible(Named::Template, c, a),
"pg_type_is_visible" => type_visible,
"pg_function_is_visible" => function_visible,
"pg_operator_is_visible" => operator_visible,
"pg_statistics_obj_is_visible" => statistics_obj_visible,
"pg_get_userbyid" => get_userbyid,
"pg_my_temp_schema" => |call, _| Ok(Value::Oid(reg::temp_namespace(call.session))),
"pg_is_other_temp_schema" => is_other_temp_schema,
"pg_get_sequence_data" => get_sequence_data,
"pg_partition_root" => partition_root,
"pg_relation_is_publishable" => relation_is_publishable,
"pg_get_serial_sequence" => serial_sequence,
_ => return None,
};
Some(kernel)
}
pub(crate) fn sql_function(func: u32) -> Option<Kernel> {
Some(match func {
1215 => obj_description,
1216 => col_description,
1348 => obj_description_any,
1993 => shobj_description,
2074 => crate::regex::similar_substring,
_ => return None,
})
}