Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All changes that a user can see are in this file. The version is 0.M.patch, wher

### Added

- The catalog information functions of spec/07 section 7.18.3: `pg_table_is_visible`, `pg_type_is_visible`, `pg_function_is_visible`, `pg_operator_is_visible`, `pg_opclass_is_visible`, `pg_opfamily_is_visible`, `pg_collation_is_visible`, `pg_conversion_is_visible`, the four `pg_ts_*_is_visible` functions and `pg_statistics_obj_is_visible`. Each one finds the object as `namespace.c` does, with the schemas of `search_path`, and gives a null for an OID that no object has. `pg_get_userbyid` gives the name of the role, or `unknown (OID=n)`. `obj_description`, `col_description` and `shobj_description` give the comments of `pg_description` and `pg_shdescription`, as the SQL functions of `system_functions.sql` do. The catalog has the built-in objects and comments only, so the objects and comments that the scripts of initdb add are not there yet.

- The input and output of the 12 OID alias types (spec/04 section 4.9): `regclass`, `regtype`, `regproc`, `regprocedure`, `regoper`, `regoperator`, `regnamespace`, `regrole`, `regcollation`, `regconfig`, `regdictionary` and `regdatabase`. The input takes a number, `-` for 0, or a name that it finds as `regproc.c` and `namespace.c` do, with the schemas of `search_path`. The output gives the name with its schema only when the name alone does not find the object, so `2836::regclass` is `pg_toast.pg_toast_1255`. The functions `to_regclass`, `to_regtype`, `to_regtypemod` and the others give a null for a name that does not exist, and the same error as PostgreSQL for a syntax error in a type name or a name with too many dots. The cast from `text` to `regclass` gives the error of `RangeVarGetRelid`. The binary format of each type is the format of `oid`. The catalog has the built-in objects only, so a name finds a built-in object or nothing.

- Subqueries in expressions (spec/23 section 23.5): `EXISTS`, `IN`, `NOT IN`, `op ANY`, `op ALL`, `ARRAY(SELECT ...)` and a scalar subquery, in the target list, `WHERE`, `JOIN ... ON`, `HAVING`, `GROUP BY`, `ORDER BY`, `LIMIT` and `OFFSET`. A subquery can read the columns of the queries outside it at any depth. The analyzer finds a name as `parse_relation.c` does, from the subquery up to the outer queries, so a bad name gives the same error and hint as PostgreSQL, also for a close name of an outer relation. It gives the errors of `transformSubLink` for a wrong number of columns, and `subquery uses ungrouped column` for a column of a grouped outer query. The executor runs a subquery that reads no outer column once, as an `InitPlan` does, and runs the other subqueries again for each row. It reads the tables of a subquery once. `EXISTS` skips the target list when `simplify_EXISTS_query` can drop it, so `EXISTS (SELECT 1/0 FROM t)` gives no error, and a scalar subquery with more than one row gives `21000`. A row on the left of `IN` or `ANY`, `ARRAY()` of an array, an aggregate of an outer query, and a subquery in `FROM` give `0A000`.
Expand Down
115 changes: 115 additions & 0 deletions crates/rupg-exec/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,121 @@ fn oid_alias_types() {
});
}

/// The catalog information functions: the `pg_*_is_visible` functions, `pg_get_userbyid`, `obj_description`, `col_description` and `shobj_description`. Each result is the result of PostgreSQL 19 for `SELECT` and the text, with `search_path` at its default. The counts use only the OIDs below 10000, because the scripts of initdb add more rows.
#[test]
fn catalog_information_functions() {
const CASES: &[(&str, &str)] = &[
("pg_table_is_visible(1259)", "t"),
("pg_table_is_visible('pg_class'::regclass)", "t"),
("pg_table_is_visible(2604)", "t"),
("pg_table_is_visible(0)", "NULL"),
("pg_table_is_visible(99999)", "NULL"),
("pg_table_is_visible(NULL)", "NULL"),
("pg_table_is_visible('pg_toast.pg_toast_1255'::regclass)", "f"),
("pg_type_is_visible(23)", "t"),
("pg_type_is_visible('int4'::regtype)", "t"),
("pg_type_is_visible(0)", "NULL"),
("pg_type_is_visible(99999)", "NULL"),
("pg_type_is_visible(1007)", "t"),
("pg_type_is_visible('pg_class'::regtype)", "t"),
("pg_function_is_visible(177)", "t"),
("pg_function_is_visible('int4pl'::regproc)", "t"),
("pg_function_is_visible(0)", "NULL"),
("pg_function_is_visible(99999)", "NULL"),
("pg_operator_is_visible(551)", "t"),
("pg_operator_is_visible(96)", "t"),
("pg_operator_is_visible(0)", "NULL"),
("pg_operator_is_visible(99999)", "NULL"),
("pg_opclass_is_visible(1978)", "t"),
("pg_opclass_is_visible(99999)", "NULL"),
("pg_opfamily_is_visible(1976)", "t"),
("pg_opfamily_is_visible(99999)", "NULL"),
("pg_conversion_is_visible(4402)", "NULL"),
("pg_conversion_is_visible(99999)", "NULL"),
("pg_ts_parser_is_visible(3722)", "t"),
("pg_ts_parser_is_visible(99999)", "NULL"),
("pg_ts_template_is_visible(3727)", "t"),
("pg_ts_template_is_visible(99999)", "NULL"),
("pg_ts_config_is_visible(3748)", "t"),
("pg_ts_config_is_visible(99999)", "NULL"),
("pg_ts_dict_is_visible(3765)", "t"),
("pg_ts_dict_is_visible(99999)", "NULL"),
("pg_collation_is_visible(100)", "t"),
("pg_collation_is_visible(950)", "t"),
("pg_collation_is_visible(99999)", "NULL"),
("pg_statistics_obj_is_visible(99999)", "NULL"),
("pg_statistics_obj_is_visible(0)", "NULL"),
("pg_get_userbyid(10)", "postgres"),
("pg_get_userbyid(0)", "unknown (OID=0)"),
("pg_get_userbyid(99999)", "unknown (OID=99999)"),
("pg_get_userbyid(NULL)", "NULL"),
("pg_typeof(pg_get_userbyid(10))", "name"),
("obj_description(1259, 'pg_class')", "NULL"),
("obj_description(11, 'pg_namespace')", "system catalog schema"),
("obj_description(2200, 'pg_namespace')", "standard public schema"),
("obj_description(99, 'pg_namespace')", "reserved schema for TOAST tables"),
("obj_description(23, 'pg_type')", "-2 billion to 2 billion integer, 4-byte storage"),
("obj_description('int4pl'::regproc, 'pg_proc')", "implementation of + operator"),
("obj_description(551, 'pg_operator')", "add"),
("obj_description(403, 'pg_am')", "b-tree index access method"),
("obj_description(23, 'pg_proc')", "NULL"),
("obj_description(23, 'no_such_catalog')", "NULL"),
("obj_description(23, NULL)", "NULL"),
("obj_description(NULL, 'pg_type')", "NULL"),
("obj_description(1, 'pg_database')", "NULL"),
("obj_description(23)", "-2 billion to 2 billion integer, 4-byte storage"),
("obj_description(551)", "add"),
("obj_description(11)", "system catalog schema"),
("obj_description(99999)", "NULL"),
("obj_description(0)", "NULL"),
("pg_typeof(obj_description(23))", "text"),
("col_description(1259, 1)", "NULL"),
("col_description(1259, 0)", "NULL"),
("col_description(99999, 1)", "NULL"),
("col_description(1259, NULL)", "NULL"),
("shobj_description(1, 'pg_database')", "default template for new databases"),
("shobj_description(10, 'pg_authid')", "NULL"),
("shobj_description(1, 'pg_class')", "NULL"),
("shobj_description(99999, 'pg_database')", "NULL"),
("shobj_description(1, NULL)", "NULL"),
(
"obj_description(oid, 'pg_proc') FROM pg_proc WHERE proname = 'int4pl'",
"implementation of + operator",
),
("count(*) FROM pg_type WHERE oid < 10000 AND pg_type_is_visible(oid)", "203"),
("count(*) FROM pg_class WHERE oid < 10000 AND pg_table_is_visible(oid)", "188"),
("count(*) FROM pg_proc WHERE oid < 1000 AND pg_function_is_visible(oid)", "580"),
("count(*) FROM pg_operator WHERE oid < 1000 AND pg_operator_is_visible(oid)", "284"),
("count(*) FROM pg_opclass WHERE pg_opclass_is_visible(oid)", "179"),
("count(*) FROM pg_opfamily WHERE pg_opfamily_is_visible(oid)", "148"),
("count(*) FROM pg_conversion WHERE pg_conversion_is_visible(oid)", "98"),
(
"count(*) FROM pg_proc WHERE oid < 1000 AND obj_description(oid, 'pg_proc') IS NOT NULL",
"580",
),
(
"count(*) FROM pg_type WHERE obj_description(oid, 'pg_type') IS NOT NULL AND oid < 1000",
"36",
),
("count(*) FROM pg_operator WHERE obj_description(oid) IS NOT NULL AND oid < 1000", "284"),
];
big_stack(|| {
for (text, expected) in CASES {
assert_eq!(outcome(&format!("SELECT {text}")), *expected, "{text}");
}
assert_eq!(
row(
"SELECT 'int4'::regtype, pg_type_is_visible('int4'::regtype), obj_description('int4'::regtype, 'pg_type')"
),
Ok(Some(vec![
"integer".into(),
"t".into(),
"-2 billion to 2 billion integer, 4-byte storage".into()
]))
);
});
}

/// The place of an error in the input of an OID alias type. An error of a literal is at the literal, as `pcb_error_callback` gives it. A syntax error in a type name at run time is at its place in the type name, with the line of `CONTEXT`. The places are the places of PostgreSQL 19, from 0.
#[test]
fn oid_alias_type_error_places() {
Expand Down
136 changes: 136 additions & 0 deletions crates/rupg-func/src/catinfo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! The catalog information functions: the `pg_*_is_visible` functions, `pg_get_userbyid`, and the comments of `obj_description`, `col_description` and `shobj_description`.
//!
//! The visibility functions are a port of the `*IsVisible` functions of `namespace.c`. Each one gives null for an OID that no object has, as the `is_missing` form of PostgreSQL does.

use rupg_common::Result;
use rupg_pgcatalog::builtin::{self, Named};
use rupg_types::Value;

use crate::reg::{self, path};
use crate::{Call, Kernel, bad_value};

/// The OID of `pg_catalog`.
const PG_CATALOG: u32 = 11;
/// The OID of `pg_class` in `pg_class`, the catalog of the comments of the columns.
const PG_CLASS: u32 = 1259;

/// The OID argument at `index`.
fn oid_arg(args: &[Value], index: usize) -> Result<u32> {
args.get(index).and_then(Value::as_oid).ok_or_else(bad_value)
}

/// The visibility of an object of a kind that [`Named`] has, such as a relation or an operator class.
fn named_visible(kind: Named, call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
Ok(builtin::named_by_oid(kind, oid)
.map_or(Value::Null, |row| Value::Bool(reg::visible(kind, row, call.session))))
}

/// `pg_type_is_visible`: true when the first type with the name in the search path is this type.
fn type_visible(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(row) = builtin::type_by_oid(oid_arg(args, 0)?) else { return Ok(Value::Null) };
let found = path(call.session).into_iter().find_map(|ns| builtin::type_by_name(ns, row.name));
Ok(Value::Bool(found.is_some_and(|t| t.oid == row.oid)))
}

/// `pg_function_is_visible`.
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))))
}

/// `pg_operator_is_visible`.
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))))
}

/// `pg_statistics_obj_is_visible`. The catalog has no extended statistics object, so each OID gives null.
fn statistics_obj_visible(_: &Call<'_>, args: &[Value]) -> Result<Value> {
oid_arg(args, 0)?;
Ok(Value::Null)
}

/// `pg_get_userbyid`: the name of the role, or `unknown (OID=n)`.
fn get_userbyid(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
Ok(Value::Text(match builtin::named_by_oid(Named::Role, oid) {
Some(row) => row.name.to_string(),
None => format!("unknown (OID={oid})"),
}))
}

/// The OID of the catalog with this name in `pg_catalog`, as the subquery of `obj_description` finds it.
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))
}

/// A comment as the result of a function, or null.
fn comment(text: Option<&str>) -> Value {
text.map_or(Value::Null, Value::text)
}

/// `obj_description(oid, name)`: the comment of an object of the catalog with the name.
fn obj_description(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
Ok(comment(catalog_oid(args)?.and_then(|class| builtin::description(oid, class, 0))))
}

/// `obj_description(oid)`: the first comment of an object with the OID in any catalog.
fn obj_description_any(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = oid_arg(args, 0)?;
let found = builtin::descriptions().iter().find(|d| d.objoid == oid && d.objsubid == 0);
Ok(comment(found.map(|d| d.description)))
}

/// `col_description(oid, integer)`: the comment of a column of a relation.
fn col_description(_: &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(comment(builtin::description(oid, PG_CLASS, column)))
}

/// `shobj_description(oid, name)`: the comment of a shared object of the catalog with the name.
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))))
}

/// The kernel of a function of this module by its `prosrc`.
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
let kernel: Kernel = match src {
"pg_table_is_visible" => |c, a| named_visible(Named::Class, c, a),
"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,
_ => return None,
};
Some(kernel)
}

/// The kernel of a function in SQL of `system_functions.sql` by its OID.
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,
_ => return None,
})
}
2 changes: 2 additions & 0 deletions crates/rupg-func/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

mod agg;
mod cast;
mod catinfo;
mod compare;
mod io;
mod math;
Expand Down Expand Up @@ -141,6 +142,7 @@ fn by_src(src: &str) -> Option<Kernel> {
.or_else(|| text::by_src(src))
.or_else(|| misc::by_src(src))
.or_else(|| reg::by_src(src))
.or_else(|| catinfo::by_src(src))
}

/// The name of the operator that the function implements, if it implements one.
Expand Down
2 changes: 1 addition & 1 deletion crates/rupg-func/src/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,6 @@ pub(crate) fn sql_function(func: u32) -> Option<Kernel> {
1810 | 1811 => bit_length,
2003 | 2004 => any_concat,
1285 | 1290 => quote_any,
_ => return math::numeric_sql(func),
_ => return math::numeric_sql(func).or_else(|| crate::catinfo::sql_function(func)),
})
}
Loading
Loading