Skip to main content

rupg_func/
describe.rs

1//! `pg_describe_object`: the description of an object that `getObjectDescription` of `objectaddress.c` gives, as the messages of the dependencies show it.
2//!
3//! A name has its schema when the search path does not find the object by its name. The name of a column, a constraint, a rule, a trigger, a schema, a role, a database, a tablespace, a language, an access method or an extension has no quotes. Each object that does not exist gives null, as `getObjectDescription` with `missing_ok` gives.
4
5use rupg_catalog::{ObjRef, RelKind};
6use rupg_common::{Error, Result, SqlState};
7use rupg_pgcatalog::builtin::{self, Named, Owned};
8use rupg_types::{RegKind, Value};
9
10use crate::{Call, Kernel, Session, bad_value, reg};
11
12/// `pg_class`.
13pub(crate) const PG_CLASS: u32 = 1259;
14/// `pg_proc`.
15pub(crate) const PG_PROC: u32 = 1255;
16/// `pg_type`.
17pub(crate) const PG_TYPE: u32 = 1247;
18/// `pg_cast`.
19pub(crate) const PG_CAST: u32 = 2605;
20/// `pg_collation`.
21pub(crate) const PG_COLLATION: u32 = 3456;
22/// `pg_constraint`.
23pub(crate) const PG_CONSTRAINT: u32 = 2606;
24/// `pg_conversion`.
25pub(crate) const PG_CONVERSION: u32 = 2607;
26/// `pg_attrdef`.
27pub(crate) const PG_ATTRDEF: u32 = 2604;
28/// `pg_language`.
29pub(crate) const PG_LANGUAGE: u32 = 2612;
30/// `pg_largeobject`.
31pub(crate) const PG_LARGEOBJECT: u32 = 2613;
32/// `pg_operator`.
33pub(crate) const PG_OPERATOR: u32 = 2617;
34/// `pg_opclass`.
35pub(crate) const PG_OPCLASS: u32 = 2616;
36/// `pg_opfamily`.
37pub(crate) const PG_OPFAMILY: u32 = 2753;
38/// `pg_am`.
39pub(crate) const PG_AM: u32 = 2601;
40/// `pg_amop`.
41pub(crate) const PG_AMOP: u32 = 2602;
42/// `pg_amproc`.
43pub(crate) const PG_AMPROC: u32 = 2603;
44/// `pg_rewrite`.
45pub(crate) const PG_REWRITE: u32 = 2618;
46/// `pg_trigger`.
47pub(crate) const PG_TRIGGER: u32 = 2620;
48/// `pg_namespace`.
49pub(crate) const PG_NAMESPACE: u32 = 2615;
50/// `pg_ts_parser`.
51pub(crate) const PG_TS_PARSER: u32 = 3601;
52/// `pg_ts_dict`.
53pub(crate) const PG_TS_DICT: u32 = 3600;
54/// `pg_ts_template`.
55pub(crate) const PG_TS_TEMPLATE: u32 = 3764;
56/// `pg_ts_config`.
57pub(crate) const PG_TS_CONFIG: u32 = 3602;
58/// `pg_authid`.
59pub(crate) const PG_AUTHID: u32 = 1260;
60/// `pg_database`.
61pub(crate) const PG_DATABASE: u32 = 1262;
62/// `pg_tablespace`.
63pub(crate) const PG_TABLESPACE: u32 = 1213;
64/// `pg_extension`.
65pub(crate) const PG_EXTENSION: u32 = 3079;
66/// `pg_statistic_ext`.
67pub(crate) const PG_STATISTIC_EXT: u32 = 3381;
68
69/// The catalogs of the kinds of objects that rupg does not have: `pg_auth_members`, `pg_foreign_data_wrapper`, `pg_foreign_server`, `pg_user_mapping`, `pg_default_acl`, `pg_event_trigger`, `pg_parameter_acl`, `pg_policy`, `pg_publication`, `pg_publication_namespace`, `pg_publication_rel`, `pg_subscription` and `pg_transform`.
70pub(crate) const NO_OBJECTS: [u32; 13] =
71    [1261, 2328, 1417, 1418, 826, 3466, 6243, 3256, 6104, 6237, 6106, 6100, 3576];
72
73/// The names of the system columns, from `ctid` with the number -1.
74pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "cmin", "xmax", "cmax", "tableoid"];
75
76/// The OID argument at `index`.
77pub(crate) fn oid_arg(args: &[Value], index: usize) -> Result<u32> {
78    args.get(index).and_then(Value::as_oid).ok_or_else(bad_value)
79}
80
81/// `pg_describe_object(oid, oid, integer)`: the description of the object, or null for OID 0 in both the catalog and the object.
82fn describe_object(call: &Call<'_>, args: &[Value]) -> Result<Value> {
83    let (class, oid) = (oid_arg(args, 0)?, oid_arg(args, 1)?);
84    let sub = args
85        .get(2)
86        .and_then(Value::as_i64)
87        .and_then(|n| i32::try_from(n).ok())
88        .ok_or_else(bad_value)?;
89    if class == 0 && oid == 0 {
90        return Ok(Value::Null);
91    }
92    Ok(describe(ObjRef { class, oid, sub }, call.session)?.map_or(Value::Null, Value::Text))
93}
94
95/// `getObjectDescription` with `missing_ok`: the description of the object, or `None` when it does not exist.
96///
97/// # Errors
98///
99/// `XX000` for a catalog that is not a catalog of objects.
100pub fn describe(object: ObjRef, session: &dyn Session) -> Result<Option<String>> {
101    let oid = object.oid;
102    let catalog = session.catalog();
103    let text = match object.class {
104        PG_CLASS if object.sub != 0 => column_text(oid, object.sub, session),
105        PG_CLASS => relation_text(oid, session),
106        PG_PROC => function_text(oid, session),
107        PG_TYPE => reg::name_of(RegKind::Type, oid, session).map(|t| format!("type {t}")),
108        PG_CAST => cast_text(oid, session),
109        PG_COLLATION => {
110            reg::catalog_text(Named::Collation, oid, session).map(|c| format!("collation {c}"))
111        }
112        PG_CONSTRAINT => catalog.and_then(|c| c.constraint(oid)).and_then(|con| {
113            if con.relation == 0 {
114                return Some(format!("constraint {}", con.name));
115            }
116            Some(format!("constraint {} on {}", con.name, relation_text(con.relation, session)?))
117        }),
118        PG_CONVERSION => {
119            named_text(Named::Conversion, oid, session).map(|c| format!("conversion {c}"))
120        }
121        PG_ATTRDEF => catalog
122            .and_then(|c| c.defaults().find(|d| d.oid == oid))
123            .and_then(|d| column_text(d.relation, i32::from(d.column), session))
124            .map(|column| format!("default value for {column}")),
125        PG_LANGUAGE => builtin::owned_by_oid(Owned::Language, oid)
126            .map(|l| l.name)
127            .or_else(|| catalog?.languages().find(|l| l.oid == oid).map(|l| l.name.as_str()))
128            .map(|name| format!("language {name}")),
129        PG_LARGEOBJECT => {
130            builtin::owned_by_oid(Owned::LargeObject, oid).map(|_| format!("large object {oid}"))
131        }
132        PG_OPERATOR => {
133            reg::name_of(RegKind::Operator, oid, session).map(|o| format!("operator {o}"))
134        }
135        PG_OPCLASS => {
136            method_text(Named::Opclass, oid, session).map(|c| format!("operator class {c}"))
137        }
138        PG_OPFAMILY => opfamily_text(oid, session),
139        PG_AM => builtin::access_method_name(oid).map(|name| format!("access method {name}")),
140        PG_AMOP => {
141            amop_parts(oid, session).and_then(|(family, left, right, strategy, operator)| {
142                Some(format!(
143                    "operator {strategy} ({}, {}) of {}: {}",
144                    reg::type_text(left, None, session).ok()?,
145                    reg::type_text(right, None, session).ok()?,
146                    opfamily_text(family, session)?,
147                    reg::name_of(RegKind::Operator, operator, session)?
148                ))
149            })
150        }
151        PG_AMPROC => amproc_parts(oid, session).and_then(|(family, left, right, number, proc)| {
152            Some(format!(
153                "function {number} ({}, {}) of {}: {}",
154                reg::type_text(left, None, session).ok()?,
155                reg::type_text(right, None, session).ok()?,
156                opfamily_text(family, session)?,
157                procedure_text(proc, session)?
158            ))
159        }),
160        PG_REWRITE => catalog.and_then(|c| match c.view_of_rule(oid) {
161            Some(view) => Some(format!("rule _RETURN on {}", relation_text(view.oid, session)?)),
162            None => {
163                let rule = c.rule(oid)?;
164                Some(format!("rule {} on {}", rule.name, relation_text(rule.relation, session)?))
165            }
166        }),
167        PG_TRIGGER => catalog.and_then(|c| c.trigger(oid)).and_then(|t| {
168            Some(format!("trigger {} on {}", t.name, relation_text(t.relation, session)?))
169        }),
170        PG_NAMESPACE => reg::namespace_name(oid, session).map(|name| format!("schema {name}")),
171        PG_TS_PARSER => ts_text(Named::Parser, "parser", oid, session),
172        PG_TS_DICT => ts_text(Named::Dictionary, "dictionary", oid, session),
173        PG_TS_TEMPLATE => ts_text(Named::Template, "template", oid, session),
174        PG_TS_CONFIG => ts_text(Named::Config, "configuration", oid, session),
175        PG_AUTHID => rupg_pgcatalog::roles::current().name(oid).map(|name| format!("role {name}")),
176        PG_DATABASE => match builtin::named_by_oid(Named::Database, oid) {
177            Some(row) => Some(format!("database {}", row.name)),
178            None => session
179                .databases()
180                .iter()
181                .find(|d| d.oid == oid)
182                .map(|d| format!("database {}", d.name)),
183        },
184        PG_TABLESPACE => {
185            crate::dbsize::tablespace_name(oid, session).map(|name| format!("tablespace {name}"))
186        }
187        PG_EXTENSION => catalog
188            .and_then(|c| c.extensions().find(|e| e.oid == oid))
189            .map(|e| format!("extension {}", e.name)),
190        PG_STATISTIC_EXT => catalog.and_then(|c| c.stat_ext(oid)).and_then(|s| {
191            let name = if reg::stat_ext_visible(oid, session)? {
192                reg::qualified(None, &s.name)
193            } else {
194                reg::qualified(reg::namespace_name(s.namespace, session), &s.name)
195            };
196            Some(format!("statistics object {name}"))
197        }),
198        rupg_catalog::PG_DEFAULT_ACL => catalog.and_then(|c| c.default_acl(oid)).and_then(|d| {
199            let kind = match d.objtype {
200                b'r' => "relations",
201                b'S' => "sequences",
202                b'f' => "functions",
203                b'T' => "types",
204                b'n' => "schemas",
205                _ => "large objects",
206            };
207            let roles = rupg_pgcatalog::roles::current();
208            let role = roles.name(d.role)?;
209            let mut text = format!("default privileges on new {kind} belonging to role {role}");
210            if d.namespace != 0 {
211                text.push_str(&format!(
212                    " in schema {}",
213                    reg::namespace_name(d.namespace, session)?
214                ));
215            }
216            Some(text)
217        }),
218        class if NO_OBJECTS.contains(&class) => None,
219        class => {
220            return Err(Error::new(
221                SqlState::INTERNAL_ERROR,
222                format!("unsupported object class: {class}"),
223            ));
224        }
225    };
226    Ok(text)
227}
228
229/// `getRelationDescription`: the kind and the name of the relation.
230fn relation_text(oid: u32, session: &dyn Session) -> Option<String> {
231    let (code, name, namespace) = match builtin::class_by_oid(oid) {
232        Some(row) => (row.kind, row.name, row.namespace),
233        None => {
234            let rel = session.catalog()?.relation(oid)?;
235            (u8::try_from(rel.kind.code()).ok()?, rel.name.as_str(), rel.namespace)
236        }
237    };
238    let word = match code {
239        b'i' | b'I' => "index",
240        b'S' => "sequence",
241        b't' => "toast table",
242        b'v' => "view",
243        b'm' => "materialized view",
244        b'c' => "composite type",
245        b'f' => "foreign table",
246        _ => "table",
247    };
248    let schema = if reg::class_visible(oid, session)? {
249        None
250    } else {
251        reg::namespace_name(namespace, session)
252    };
253    Some(format!("{word} {}", reg::qualified(schema, name)))
254}
255
256/// The description of a column of a relation, also of a system column of a relation that has them.
257fn column_text(oid: u32, number: i32, session: &dyn Session) -> Option<String> {
258    let name = column_name(oid, number, session)?;
259    Some(format!("column {name} of {}", relation_text(oid, session)?))
260}
261
262/// The name of a column of a relation, also of a system column of a relation that has them, or `None` when the relation has no column with the number.
263pub(crate) fn column_name(oid: u32, number: i32, session: &dyn Session) -> Option<&str> {
264    let number = i16::try_from(number).ok()?;
265    let name = match builtin::class_by_oid(oid) {
266        Some(_) => builtin::attributes(oid).iter().find(|a| a.num == number)?.name,
267        None => {
268            let rel = session.catalog()?.relation(oid)?;
269            if number > 0 {
270                rel.column(number)?.name.as_str()
271            } else if matches!(
272                rel.kind,
273                RelKind::Table
274                    | RelKind::Partitioned
275                    | RelKind::Toast
276                    | RelKind::Sequence
277                    | RelKind::MatView
278            ) {
279                SYSTEM_COLUMNS.get(usize::try_from(-i32::from(number) - 1).ok()?)?
280            } else {
281                return None;
282            }
283        }
284    };
285    Some(name)
286}
287
288/// `format_procedure` after the word `function`, as `getObjectDescription` gives it for each kind of routine.
289fn function_text(oid: u32, session: &dyn Session) -> Option<String> {
290    Some(format!("function {}", procedure_text(oid, session)?))
291}
292
293/// `format_procedure`: the name of a built-in function or a function of the catalog, with its schema when it is not visible, and the types of its arguments.
294fn procedure_text(oid: u32, session: &dyn Session) -> Option<String> {
295    let catalog = session.catalog();
296    let (name, namespace, args) = match builtin::proc_by_oid(oid) {
297        Some(proc) => (proc.name, proc.namespace, proc.argtypes),
298        None => {
299            let function = catalog?.function(oid)?;
300            (function.name.as_str(), function.namespace, function.argtypes.as_slice())
301        }
302    };
303    let first = reg::path(session).into_iter().find_map(|ns| {
304        builtin::procs_named(name)
305            .find(|p| p.namespace == ns && p.argtypes == args)
306            .map(|p| p.oid)
307            .or_else(|| {
308                catalog?
309                    .functions()
310                    .find(|f| f.namespace == ns && f.name == name && f.argtypes == args)
311                    .map(|f| f.oid)
312            })
313    });
314    let schema = if first == Some(oid) { None } else { reg::namespace_name(namespace, session) };
315    let args: Vec<String> =
316        args.iter().map(|&ty| reg::type_text(ty, None, session)).collect::<Result<_>>().ok()?;
317    Some(format!("{}({})", reg::qualified(schema, name), args.join(",")))
318}
319
320/// `getCastDescription`.
321fn cast_text(oid: u32, session: &dyn Session) -> Option<String> {
322    let (source, target) = match builtin::casts().iter().find(|c| c.oid == oid) {
323        Some(cast) => (cast.source, cast.target),
324        None => {
325            let cast = session.catalog()?.casts().find(|c| c.oid == oid)?;
326            (cast.source, cast.target)
327        }
328    };
329    Some(format!(
330        "cast from {} to {}",
331        reg::type_text(source, None, session).ok()?,
332        reg::type_text(target, None, session).ok()?
333    ))
334}
335
336/// The name of a built-in object of a kind that has only built-in objects, with its schema when it is not visible.
337fn named_text(kind: Named, oid: u32, session: &dyn Session) -> Option<String> {
338    let row = builtin::named_by_oid(kind, oid)?;
339    let schema = if reg::visible(kind, row, session) {
340        None
341    } else {
342        reg::namespace_name(row.namespace, session)
343    };
344    Some(reg::qualified(schema, row.name))
345}
346
347/// The name of an operator class or an operator family and its access method.
348fn method_text(kind: Named, oid: u32, session: &dyn Session) -> Option<String> {
349    let (name, namespace, method) = reg::method_row(kind, oid, session)?;
350    let schema = if reg::method_visible(kind, oid, session) {
351        None
352    } else {
353        reg::namespace_name(namespace, session)
354    };
355    Some(format!(
356        "{} for access method {}",
357        reg::qualified(schema, &name),
358        builtin::access_method_name(method)?
359    ))
360}
361
362/// The family, the left type, the right type, the number and the operator of a row of `pg_amop`, built in or of the catalog.
363pub(crate) fn amop_parts(oid: u32, session: &dyn Session) -> Option<(u32, u32, u32, i16, u32)> {
364    if let Some(a) = builtin::amop_by_oid(oid) {
365        return Some((a.family, a.left, a.right, a.strategy, a.operator));
366    }
367    let a = session.catalog()?.amop(oid)?;
368    Some((a.family, a.left, a.right, a.strategy, a.operator))
369}
370
371/// The family, the left type, the right type, the number and the function of a row of `pg_amproc`, built in or of the catalog.
372pub(crate) fn amproc_parts(oid: u32, session: &dyn Session) -> Option<(u32, u32, u32, i16, u32)> {
373    if let Some(a) = builtin::amproc_by_oid(oid) {
374        return Some((a.family, a.left, a.right, a.number, a.proc));
375    }
376    let a = session.catalog()?.amproc(oid)?;
377    Some((a.family, a.left, a.right, a.number, a.function))
378}
379
380/// `getOpFamilyDescription`.
381fn opfamily_text(oid: u32, session: &dyn Session) -> Option<String> {
382    method_text(Named::Opfamily, oid, session).map(|f| format!("operator family {f}"))
383}
384
385/// The description of an object of text search.
386fn ts_text(kind: Named, word: &str, oid: u32, session: &dyn Session) -> Option<String> {
387    reg::catalog_text(kind, oid, session).map(|name| format!("text search {word} {name}"))
388}
389
390/// The kernel of a function of this module by its `prosrc`.
391pub(crate) fn by_src(src: &str) -> Option<Kernel> {
392    let kernel: Kernel = match src {
393        "pg_describe_object" => describe_object,
394        _ => return None,
395    };
396    Some(kernel)
397}