diff --git a/CHANGELOG.md b/CHANGELOG.md index caa51dd5..7a0fb659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/crates/rupg-exec/src/tests.rs b/crates/rupg-exec/src/tests.rs index 6bd596ed..8240eb4c 100644 --- a/crates/rupg-exec/src/tests.rs +++ b/crates/rupg-exec/src/tests.rs @@ -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() { diff --git a/crates/rupg-func/src/catinfo.rs b/crates/rupg-func/src/catinfo.rs new file mode 100644 index 00000000..9b1ad9ed --- /dev/null +++ b/crates/rupg-func/src/catinfo.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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> { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + Some(match func { + 1215 => obj_description, + 1216 => col_description, + 1348 => obj_description_any, + 1993 => shobj_description, + _ => return None, + }) +} diff --git a/crates/rupg-func/src/lib.rs b/crates/rupg-func/src/lib.rs index 742a0bee..8b04ac10 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -6,6 +6,7 @@ mod agg; mod cast; +mod catinfo; mod compare; mod io; mod math; @@ -141,6 +142,7 @@ fn by_src(src: &str) -> Option { .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. diff --git a/crates/rupg-func/src/misc.rs b/crates/rupg-func/src/misc.rs index 0afaf513..c67f723d 100644 --- a/crates/rupg-func/src/misc.rs +++ b/crates/rupg-func/src/misc.rs @@ -174,6 +174,6 @@ pub(crate) fn sql_function(func: u32) -> Option { 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)), }) } diff --git a/crates/rupg-func/src/reg.rs b/crates/rupg-func/src/reg.rs index 992f0d3d..96858934 100644 --- a/crates/rupg-func/src/reg.rs +++ b/crates/rupg-func/src/reg.rs @@ -55,12 +55,12 @@ fn namespace_oid(name: &str) -> Option { } /// The name of the schema with this OID. -fn namespace_name(oid: u32) -> Option<&'static str> { +pub(crate) fn namespace_name(oid: u32) -> Option<&'static str> { builtin::named_by_oid(Named::Namespace, oid).map(|row| row.name) } /// The schemas that an unqualified name searches: `pg_catalog` first when `search_path` does not name it, then the schemas of `search_path` that exist. -fn path(session: &dyn Session) -> Vec { +pub(crate) fn path(session: &dyn Session) -> Vec { let mut path: Vec = session.schemas().iter().filter_map(|n| namespace_oid(n)).collect(); if !path.contains(&PG_CATALOG) { path.insert(0, PG_CATALOG); @@ -122,20 +122,22 @@ fn spaces(schema: Option<&str>, session: &dyn Session) -> Vec { } } -/// The object of a kind with a name in the first schema of `spaces` that has one. A collation of the encoding of the database comes before a collation of any encoding in the same schema, as `lookup_collation` finds it. -fn find(kind: Named, spaces: &[u32], name: &str) -> Option<&'static NamedRow> { +/// The object of a kind with a name in the first schema of `spaces` that has one. An operator class or an operator family must have the access method `method`, which is 0 for the other kinds. A collation of the encoding of the database comes before a collation of any encoding in the same schema, as `lookup_collation` finds it. +fn find(kind: Named, spaces: &[u32], name: &str, method: u32) -> Option<&'static NamedRow> { let rows = builtin::named(kind); spaces.iter().find_map(|&ns| { let here = |encoding: i32| { - rows.iter().find(|r| r.namespace == ns && r.name == name && r.encoding == encoding) + rows.iter().find(|r| { + r.namespace == ns && r.name == name && r.method == method && r.encoding == encoding + }) }; if kind == Named::Collation { here(UTF8).or_else(|| here(-1)) } else { here(-1) } }) } /// `RelationIsVisible` and the other checks of `namespace.c`: true when the unqualified name finds the object. -fn visible(kind: Named, row: &NamedRow, session: &dyn Session) -> bool { - find(kind, &path(session), row.name).is_some_and(|found| found.oid == row.oid) +pub(crate) fn visible(kind: Named, row: &NamedRow, session: &dyn Session) -> bool { + find(kind, &path(session), row.name, row.method).is_some_and(|found| found.oid == row.oid) } /// `FuncnameGetCandidates`: the functions with the name in the schemas, in the order of the schemas. A function with the same argument types as a function in a schema before it is hidden. @@ -169,7 +171,7 @@ fn operators(spaces: &[u32], name: &str) -> Vec<&'static OperatorRow> { } /// `FunctionIsVisible`. -fn function_visible(proc: &ProcRow, session: &dyn Session) -> bool { +pub(crate) fn function_visible(proc: &ProcRow, session: &dyn Session) -> bool { functions(&path(session), proc.name, Some(proc.argtypes.len())) .iter() .find(|f| f.argtypes == proc.argtypes) @@ -177,7 +179,7 @@ fn function_visible(proc: &ProcRow, session: &dyn Session) -> bool { } /// `OperatorIsVisible`. -fn operator_visible(op: &OperatorRow, session: &dyn Session) -> bool { +pub(crate) fn operator_visible(op: &OperatorRow, session: &dyn Session) -> bool { operators(&path(session), op.name) .iter() .find(|f| (f.left, f.right) == (op.left, op.right)) @@ -291,7 +293,7 @@ fn name_in(kind: RegKind, text: &str, names: &[Cow<'_, str>], session: &dyn Sess }; let in_schema = |named: Named, state: SqlState, message: String| { let (schema, name) = deconstruct(names, session)?; - match find(named, &spaces(schema, session), name) { + match find(named, &spaces(schema, session), name, 0) { Some(row) => Ok(Ok(row.oid)), None => soft(state, message), } @@ -385,7 +387,7 @@ fn class_in(names: &[Cow<'_, str>], session: &dyn Session, strict: bool) -> Soft }, None => path(session), }; - if let Some(row) = find(Named::Class, &spaces, name) { + if let Some(row) = find(Named::Class, &spaces, name, 0) { return Ok(Ok(row.oid)); } let message = if strict { @@ -539,7 +541,7 @@ fn operator_in(text: &str, session: &dyn Session) -> Soft { } /// The text argument of a function. -fn text_arg(args: &[Value]) -> Result<&str> { +pub(crate) fn text_arg(args: &[Value]) -> Result<&str> { args.first().and_then(Value::as_str).ok_or_else(bad_value) } diff --git a/crates/rupg-pgcatalog/src/builtin.rs b/crates/rupg-pgcatalog/src/builtin.rs index f38ef401..96780f76 100644 --- a/crates/rupg-pgcatalog/src/builtin.rs +++ b/crates/rupg-pgcatalog/src/builtin.rs @@ -158,7 +158,7 @@ pub struct AggregateRow { pub initval: Option<&'static str>, } -/// A built-in object of a kind that an OID alias type names: a relation, a schema, a role, a database, a collation, a text search configuration or a text search dictionary. +/// A built-in object that has a name, of a kind that an OID alias type or a function such as `pg_opclass_is_visible` reads. #[derive(Debug)] pub struct NamedRow { pub oid: u32, @@ -167,6 +167,8 @@ pub struct NamedRow { pub namespace: u32, /// `collencoding` of a collation, -1 for a collation of any encoding. It is -1 for the other kinds. pub encoding: i32, + /// The access method of an operator class or an operator family, or 0 for the other kinds. + pub method: u32, } /// The kinds of [`NamedRow`]. @@ -186,23 +188,38 @@ pub enum Named { Config, /// `pg_ts_dict`. Dictionary, + /// `pg_ts_parser`. + Parser, + /// `pg_ts_template`. + Template, + /// `pg_opclass`. + Opclass, + /// `pg_opfamily`. + Opfamily, + /// `pg_conversion`. + Conversion, } impl Named { - /// The catalog, the column of the name and the column of the schema, or `""` for a kind that has no schema. - fn columns(self) -> (&'static str, &'static str, &'static str) { + /// The catalog, the column of the name, the column of the schema and the column of the access method. A column that the kind does not have is `""`. + fn columns(self) -> (&'static str, &'static str, &'static str, &'static str) { match self { - Named::Class => ("pg_class", "relname", "relnamespace"), - Named::Namespace => ("pg_namespace", "nspname", ""), - Named::Role => ("pg_authid", "rolname", ""), - Named::Database => ("pg_database", "datname", ""), - Named::Collation => ("pg_collation", "collname", "collnamespace"), - Named::Config => ("pg_ts_config", "cfgname", "cfgnamespace"), - Named::Dictionary => ("pg_ts_dict", "dictname", "dictnamespace"), + Named::Class => ("pg_class", "relname", "relnamespace", ""), + Named::Namespace => ("pg_namespace", "nspname", "", ""), + Named::Role => ("pg_authid", "rolname", "", ""), + Named::Database => ("pg_database", "datname", "", ""), + Named::Collation => ("pg_collation", "collname", "collnamespace", ""), + Named::Config => ("pg_ts_config", "cfgname", "cfgnamespace", ""), + Named::Dictionary => ("pg_ts_dict", "dictname", "dictnamespace", ""), + Named::Parser => ("pg_ts_parser", "prsname", "prsnamespace", ""), + Named::Template => ("pg_ts_template", "tmplname", "tmplnamespace", ""), + Named::Opclass => ("pg_opclass", "opcname", "opcnamespace", "opcmethod"), + Named::Opfamily => ("pg_opfamily", "opfname", "opfnamespace", "opfmethod"), + Named::Conversion => ("pg_conversion", "conname", "connamespace", ""), } } - const ALL: [Named; 7] = [ + const ALL: [Named; 12] = [ Named::Class, Named::Namespace, Named::Role, @@ -210,9 +227,23 @@ impl Named { Named::Collation, Named::Config, Named::Dictionary, + Named::Parser, + Named::Template, + Named::Opclass, + Named::Opfamily, + Named::Conversion, ]; } +/// A row of `pg_description`, or of `pg_shdescription` with `objsubid` 0. +#[derive(Debug)] +pub struct DescriptionRow { + pub objoid: u32, + pub classoid: u32, + pub objsubid: i32, + pub description: &'static str, +} + struct Builtin { types: Vec, procs: Vec, @@ -233,6 +264,9 @@ struct Builtin { aggregate_fn: HashMap, primary_keys: HashMap>, named: HashMap>, + descriptions: Vec, + description_key: HashMap<(u32, u32, i32), usize>, + shared_descriptions: Vec, } static BUILTIN: LazyLock = LazyLock::new(Builtin::load); @@ -318,6 +352,7 @@ impl Builtin { let opclasses = load_opclasses(); let amops = load_amops(); let aggregates = load_aggregates(); + let descriptions = load_descriptions("pg_description"); let mut amop_member = HashMap::new(); let mut amop_operator: HashMap> = HashMap::new(); for (i, a) in amops.iter().enumerate() { @@ -352,6 +387,13 @@ impl Builtin { aggregates, primary_keys: load_primary_keys(), named: Named::ALL.into_iter().map(|kind| (kind, load_named(kind))).collect(), + description_key: descriptions + .iter() + .enumerate() + .map(|(i, d)| ((d.objoid, d.classoid, d.objsubid), i)) + .collect(), + descriptions, + shared_descriptions: load_descriptions("pg_shdescription"), types, procs, operators, @@ -555,10 +597,11 @@ fn load_aggregates() -> Vec { /// The built-in objects of a kind, from its catalog. fn load_named(kind: Named) -> Vec { - let (catalog, name, namespace) = kind.columns(); + let (catalog, name, namespace, method) = kind.columns(); let r = Rows::new(catalog); let (oid, name) = (r.oid("oid"), r.text(name)); let namespace = if namespace.is_empty() { None } else { Some(r.oid(namespace)) }; + let method = if method.is_empty() { None } else { Some(r.oid(method)) }; let encoding = (kind == Named::Collation).then(|| r.int4("collencoding")); (0..r.catalog.len) .map(|i| NamedRow { @@ -566,6 +609,23 @@ fn load_named(kind: Named) -> Vec { name: name[i], namespace: namespace.map_or(0, |n| n[i]), encoding: encoding.map_or(-1, |e| e[i]), + method: method.map_or(0, |m| m[i]), + }) + .collect() +} + +/// The rows of `pg_description` or `pg_shdescription`, in the order of the catalog. +fn load_descriptions(catalog: &str) -> Vec { + let r = Rows::new(catalog); + let (objoid, classoid, description) = + (r.oid("objoid"), r.oid("classoid"), r.text("description")); + let objsubid = (catalog == "pg_description").then(|| r.int4("objsubid")); + (0..r.catalog.len) + .map(|i| DescriptionRow { + objoid: objoid[i], + classoid: classoid[i], + objsubid: objsubid.map_or(0, |s| s[i]), + description: description[i], }) .collect() } @@ -679,6 +739,28 @@ pub fn named_by_oid(kind: Named, oid: u32) -> Option<&'static NamedRow> { named(kind).iter().find(|row| row.oid == oid) } +/// The comment of an object from `pg_description`: the OID of the object, the OID of its catalog and the column number, or 0 for the object itself. +pub fn description(objoid: u32, classoid: u32, objsubid: i32) -> Option<&'static str> { + BUILTIN + .description_key + .get(&(objoid, classoid, objsubid)) + .map(|&i| BUILTIN.descriptions[i].description) +} + +/// The rows of `pg_description`, in the order of the catalog. +pub fn descriptions() -> &'static [DescriptionRow] { + &BUILTIN.descriptions +} + +/// The comment of a shared object from `pg_shdescription`: the OID of the object and the OID of its catalog. +pub fn shared_description(objoid: u32, classoid: u32) -> Option<&'static str> { + BUILTIN + .shared_descriptions + .iter() + .find(|d| d.objoid == objoid && d.classoid == classoid) + .map(|d| d.description) +} + /// The attribute numbers of the primary key of the table, when the key is not deferrable, as `check_functional_grouping` finds them. pub fn primary_key(relid: u32) -> Option<&'static [i16]> { BUILTIN.primary_keys.get(&relid).map(Vec::as_slice)