Skip to main content

rupg_func/
reg.rs

1//! The text input and output of the OID alias types, such as `regclass` and `regprocedure`, and the functions `to_regclass` and the others, which give a null for a name that does not exist.
2//!
3//! A port of `src/backend/utils/adt/regproc.c` and of the lookups of `namespace.c` that it calls. A schema, a relation or a type is a built-in object or an object of the catalog of the session. The other kinds have only the built-in objects.
4//!
5//! The input separates two kinds of errors, as the `escontext` of PostgreSQL does. A soft error, such as a name that does not exist, is an error of the input function and a null of `to_regclass`. A hard error, such as a syntax error in a type name, is an error of both.
6
7use std::borrow::Cow;
8
9use rupg_analyze::Env;
10use rupg_catalog::Catalog;
11use rupg_common::{Error, Result, SqlState};
12use rupg_pgcatalog::builtin::{self, Named, NamedRow, OperatorRow, ProcRow};
13use rupg_types::{self as types, Record, RegInput, RegKind, Value, oid, qualified_name_list};
14
15use crate::text::quote_identifier;
16use crate::{Call, Kernel, Session, bad_value, type_error};
17
18/// The OID of `pg_catalog`.
19const PG_CATALOG: u32 = 11;
20/// `PG_UTF8`, the encoding of the database.
21const UTF8: i32 = 6;
22/// The name of the encoding of the database, as `GetDatabaseEncodingName` gives it.
23const UTF8_NAME: &str = "UTF8";
24/// `FUNC_MAX_ARGS`.
25const FUNC_MAX_ARGS: usize = 100;
26
27/// The result of the input: the outer error is a hard error, and the inner error is a soft error.
28type Soft<T> = Result<Result<T>>;
29
30/// The session as the analyzer reads it, for the type names in the input of `regtype` and `regprocedure` and for the query of a view.
31pub(crate) struct SessionEnv<'a>(pub(crate) &'a dyn Session);
32
33impl Env for SessionEnv<'_> {
34    fn search_path(&self) -> Vec<u32> {
35        self.0.schemas().iter().filter_map(|name| namespace_oid(name, self.0)).collect()
36    }
37
38    fn namespace(&self, name: &str) -> Option<u32> {
39        namespace_oid(name, self.0)
40    }
41
42    fn database(&self) -> String {
43        self.0.database().to_string()
44    }
45
46    fn input(&self, ty: u32, text: &str, typmod: i32) -> Result<Value> {
47        crate::io::input(ty, text, typmod, self.0)
48    }
49
50    fn catalog(&self) -> Option<&Catalog> {
51        self.0.catalog()
52    }
53
54    fn temp_number(&self) -> Option<i32> {
55        self.0.temp_number()
56    }
57}
58
59/// `myTempNamespace`: the OID of the temporary schema of the session, or 0 when it has none yet.
60pub(crate) fn temp_namespace(session: &dyn Session) -> u32 {
61    session
62        .catalog()
63        .map_or(0, |catalog| rupg_analyze::temp_namespace(&SessionEnv(session), catalog))
64}
65
66/// `isTempOrTempToastNamespace`: true for the temporary schema of the session and its TOAST schema.
67pub(crate) fn is_own_temp(oid: u32, session: &dyn Session) -> bool {
68    let Some(n) = session.temp_number() else { return false };
69    namespace_name(oid, session)
70        .is_some_and(|name| name == format!("pg_temp_{n}") || name == format!("pg_toast_temp_{n}"))
71}
72
73/// The OID of the schema with this name. `pg_temp` names the temporary schema of the session.
74fn namespace_oid(name: &str, session: &dyn Session) -> Option<u32> {
75    if name == "pg_temp" {
76        return Some(temp_namespace(session)).filter(|&oid| oid != 0);
77    }
78    builtin::named(Named::Namespace)
79        .iter()
80        .find(|row| row.name == name && !schema_dropped(row.oid, session))
81        .map(|row| row.oid)
82        .or_else(|| session.catalog()?.schema_by_name(name).map(|s| s.oid))
83}
84
85/// The name of the schema with this OID.
86pub(crate) fn namespace_name(oid: u32, session: &dyn Session) -> Option<&str> {
87    builtin::named_by_oid(Named::Namespace, oid)
88        .filter(|row| !schema_dropped(row.oid, session))
89        .map(|row| row.name)
90        .or_else(|| session.catalog()?.schema(oid).map(|s| s.name.as_str()))
91}
92
93/// True when `DROP SCHEMA` removed the built-in schema with the OID.
94fn schema_dropped(oid: u32, session: &dyn Session) -> bool {
95    session.catalog().is_some_and(|catalog| catalog.schema_dropped(oid))
96}
97
98/// The schemas that an unqualified name of a function, an operator or another object that is not a relation or a type searches: `pg_catalog` first when `search_path` does not name it, then the schemas of `search_path` that exist. The lookups of `namespace.c` skip the temporary schema.
99pub(crate) fn path(session: &dyn Session) -> Vec<u32> {
100    let mut path = temp_path(session);
101    let temp = temp_namespace(session);
102    path.retain(|&ns| ns != temp);
103    path
104}
105
106/// `StatisticsObjIsVisible`: true when the first statistics object with the name in [`path`] is this object, or `None` for an OID that is not a statistics object.
107pub(crate) fn stat_ext_visible(oid: u32, session: &dyn Session) -> Option<bool> {
108    let catalog = session.catalog()?;
109    let stat = catalog.stat_ext(oid)?;
110    let first = path(session)
111        .into_iter()
112        .find(|&ns| ns == stat.namespace || catalog.stat_ext_by_name(ns, &stat.name).is_some());
113    Some(first == Some(stat.namespace))
114}
115
116/// `activeSearchPath`: the schemas that an unqualified name of a relation or a type searches. The temporary schema of the session comes first, then `pg_catalog`, when `search_path` does not name them.
117pub(crate) fn temp_path(session: &dyn Session) -> Vec<u32> {
118    let mut path: Vec<u32> =
119        session.schemas().iter().filter_map(|n| namespace_oid(n, session)).collect();
120    if !path.contains(&PG_CATALOG) {
121        path.insert(0, PG_CATALOG);
122    }
123    let temp = temp_namespace(session);
124    if temp != 0 && !path.contains(&temp) {
125        path.insert(0, temp);
126    }
127    path
128}
129
130/// `quote_qualified_identifier`: the name with its schema, or the name alone when `schema` is `None`.
131pub(crate) fn qualified(schema: Option<&str>, name: &str) -> String {
132    match schema {
133        Some(schema) => format!("{}.{}", quote_identifier(schema), quote_identifier(name)),
134        None => quote_identifier(name),
135    }
136}
137
138/// `NameListToString`: the names with a dot between them, without quotes.
139fn name_list(names: &[Cow<'_, str>]) -> String {
140    names.join(".")
141}
142
143/// The error of a soft error with this SQLSTATE and message.
144fn soft<T>(state: SqlState, message: impl Into<String>) -> Soft<T> {
145    Ok(Err(Error::new(state, message)))
146}
147
148/// `DeconstructQualifiedName`: the schema and the name of a list of names.
149///
150/// # Errors
151///
152/// `0A000` for a database that is not the current database, and `42601` for more than three names.
153fn deconstruct<'n>(
154    names: &'n [Cow<'_, str>],
155    session: &dyn Session,
156) -> Result<(Option<&'n str>, &'n str)> {
157    match names {
158        [name] => Ok((None, name)),
159        [schema, name] => Ok((Some(schema), name)),
160        [database, schema, name] => {
161            if database != session.database() {
162                return Err(Error::new(
163                    SqlState::FEATURE_NOT_SUPPORTED,
164                    format!("cross-database references are not implemented: {}", name_list(names)),
165                ));
166            }
167            Ok((Some(schema), name))
168        }
169        _ => Err(Error::new(
170            SqlState::SYNTAX_ERROR,
171            format!("improper qualified name (too many dotted names): {}", name_list(names)),
172        )),
173    }
174}
175
176/// The schemas that a name searches: the schema that the name gives, or none when it does not exist, as the lookups with `missing_ok` give. A name without a schema searches the path.
177fn spaces(schema: Option<&str>, session: &dyn Session) -> Vec<u32> {
178    match schema {
179        Some(schema) => namespace_oid(schema, session).into_iter().collect(),
180        None => path(session),
181    }
182}
183
184/// 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.
185fn find(kind: Named, spaces: &[u32], name: &str, method: u32) -> Option<&'static NamedRow> {
186    let rows = builtin::named(kind);
187    spaces.iter().find_map(|&ns| {
188        let here = |encoding: i32| {
189            rows.iter().find(|r| {
190                r.namespace == ns && r.name == name && r.method == method && r.encoding == encoding
191            })
192        };
193        if kind == Named::Collation { here(UTF8).or_else(|| here(-1)) } else { here(-1) }
194    })
195}
196
197/// The checks of `namespace.c` for the kinds that have only built-in objects: true when the unqualified name finds the object.
198pub(crate) fn visible(kind: Named, row: &NamedRow, session: &dyn Session) -> bool {
199    find(kind, &path(session), row.name, row.method).is_some_and(|found| found.oid == row.oid)
200}
201
202/// The name, the schema and the access method of an operator class or an operator family, built in or of the catalog.
203pub(crate) fn method_row(
204    kind: Named,
205    oid: u32,
206    session: &dyn Session,
207) -> Option<(String, u32, u32)> {
208    if let Some(row) = builtin::named_by_oid(kind, oid) {
209        return Some((row.name.to_string(), row.namespace, row.method));
210    }
211    let catalog = session.catalog()?;
212    match kind {
213        Named::Opclass => catalog.opclass(oid).map(|c| (c.name.clone(), c.namespace, c.method)),
214        Named::Opfamily => catalog.opfamily(oid).map(|f| (f.name.clone(), f.namespace, f.method)),
215        _ => None,
216    }
217}
218
219/// `OpclassIsVisible` and `OpfamilyIsVisible` for an operator class or an operator family, built in or of the catalog: true when the first schema of the search path with an object of the name and the access method has this one.
220pub(crate) fn method_visible(kind: Named, oid: u32, session: &dyn Session) -> bool {
221    let Some((name, _, method)) = method_row(kind, oid, session) else { return false };
222    let catalog = session.catalog();
223    let first = path(session).into_iter().find_map(|ns| {
224        find(kind, &[ns], &name, method).map(|row| row.oid).or_else(|| match kind {
225            Named::Opclass => catalog?
226                .opclasses()
227                .find(|c| c.namespace == ns && c.name == name && c.method == method)
228                .map(|c| c.oid),
229            _ => catalog?
230                .opfamilies()
231                .find(|f| f.namespace == ns && f.name == name && f.method == method)
232                .map(|f| f.oid),
233        })
234    });
235    first == Some(oid)
236}
237
238/// The name and the schema of a collation or an object of text search, built in or of the catalog.
239pub(crate) fn catalog_name(kind: Named, oid: u32, session: &dyn Session) -> Option<(String, u32)> {
240    if let Some(row) = builtin::named_by_oid(kind, oid) {
241        return Some((row.name.to_string(), row.namespace));
242    }
243    let catalog = session.catalog()?;
244    let pair = |name: &str, namespace: u32| (name.to_string(), namespace);
245    match kind {
246        Named::Template => catalog.ts_template(oid).map(|t| pair(&t.name, t.namespace)),
247        Named::Parser => catalog.ts_parser(oid).map(|p| pair(&p.name, p.namespace)),
248        Named::Dictionary => catalog.ts_dictionary(oid).map(|d| pair(&d.name, d.namespace)),
249        Named::Config => catalog.ts_config(oid).map(|c| pair(&c.name, c.namespace)),
250        Named::Collation => catalog.collation(oid).map(|c| pair(&c.name, c.namespace)),
251        _ => None,
252    }
253}
254
255/// The OID of the collation or the object of text search with the name in the first schema of `spaces` that has one, built in or of the catalog.
256fn catalog_find(kind: Named, spaces: &[u32], name: &str, session: &dyn Session) -> Option<u32> {
257    let catalog = session.catalog();
258    spaces.iter().find_map(|&ns| {
259        find(kind, &[ns], name, 0).map(|row| row.oid).or_else(|| match kind {
260            Named::Template => catalog?.ts_template_by_name(ns, name).map(|t| t.oid),
261            Named::Parser => catalog?.ts_parser_by_name(ns, name).map(|p| p.oid),
262            Named::Dictionary => catalog?.ts_dictionary_by_name(ns, name).map(|d| d.oid),
263            Named::Config => catalog?.ts_config_by_name(ns, name).map(|c| c.oid),
264            Named::Collation => catalog?.collation_by_name(ns, name).map(|c| c.oid),
265            _ => None,
266        })
267    })
268}
269
270/// `check_default_text_search_config`: the value that `default_text_search_config` keeps for the configuration with the name in `text`. The value is the name with its schema, so a later change of `search_path` does not change the configuration. `None` when no configuration has the name.
271///
272/// # Errors
273///
274/// The errors of the input of `regconfig` that are not soft.
275pub fn ts_config_setting(text: &str, session: &dyn Session) -> Result<Option<String>> {
276    let Ok(oid) = input(RegKind::Config, text, session)? else { return Ok(None) };
277    let Some((name, namespace)) = catalog_name(Named::Config, oid, session) else {
278        return Ok(None);
279    };
280    Ok(Some(qualified(namespace_name(namespace, session), &name)))
281}
282
283/// `get_ts_parser_oid` of the name in a text, as `ts_parse` and `ts_token_type` find a parser by name.
284///
285/// # Errors
286///
287/// `42602` for a name that is not valid, `3F000` for a schema that does not exist, and `42704` for a parser that does not exist.
288pub(crate) fn ts_parser_oid(text: &str, session: &dyn Session) -> Result<u32> {
289    let names = qualified_name_list(text).map_err(type_error)?;
290    let (schema, name) = deconstruct(&names, session)?;
291    if let Some(schema) = schema
292        && namespace_oid(schema, session).is_none()
293    {
294        return Err(Error::new(
295            SqlState::UNDEFINED_SCHEMA,
296            format!("schema \"{schema}\" does not exist"),
297        ));
298    }
299    catalog_find(Named::Parser, &spaces(schema, session), name, session).ok_or_else(|| {
300        Error::new(
301            SqlState::UNDEFINED_OBJECT,
302            format!("text search parser \"{}\" does not exist", name_list(&names)),
303        )
304    })
305}
306
307/// `CollationIsVisible`, `TSParserIsVisible`, `TSDictionaryIsVisible`, `TSTemplateIsVisible` and `TSConfigIsVisible`: true when the unqualified name finds the object. `None` when no object has the OID.
308pub(crate) fn catalog_visible(kind: Named, oid: u32, session: &dyn Session) -> Option<bool> {
309    let (name, _) = catalog_name(kind, oid, session)?;
310    Some(catalog_find(kind, &path(session), &name, session) == Some(oid))
311}
312
313/// The text of `regcollationout`, `regconfigout` and `regdictionaryout`: the name, with the schema when the name is not visible.
314pub(crate) fn catalog_text(kind: Named, oid: u32, session: &dyn Session) -> Option<String> {
315    let (name, namespace) = catalog_name(kind, oid, session)?;
316    let schema = if catalog_find(kind, &path(session), &name, session) == Some(oid) {
317        None
318    } else {
319        namespace_name(namespace, session)
320    };
321    Some(qualified(schema, &name))
322}
323
324/// The OID of the relation with the name in the first schema of `spaces` that has one.
325fn find_class(spaces: &[u32], name: &str, session: &dyn Session) -> Option<u32> {
326    spaces.iter().find_map(|&ns| {
327        find(Named::Class, &[ns], name, 0)
328            .map(|row| row.oid)
329            .or_else(|| session.catalog()?.relation_by_name(ns, name).map(|r| r.oid))
330    })
331}
332
333/// The OID of the type with the name in the first schema of `spaces` that has one.
334fn find_type(spaces: &[u32], name: &str, session: &dyn Session) -> Option<u32> {
335    spaces.iter().find_map(|&ns| {
336        builtin::type_by_name(ns, name)
337            .map(|row| row.oid)
338            .or_else(|| session.catalog()?.type_by_name(ns, name).map(|t| t.oid))
339    })
340}
341
342/// The name and the schema of the relation with this OID.
343pub(crate) fn class_name(oid: u32, session: &dyn Session) -> Option<(&str, u32)> {
344    builtin::named_by_oid(Named::Class, oid)
345        .map(|row| (row.name, row.namespace))
346        .or_else(|| session.catalog()?.relation(oid).map(|r| (r.name.as_str(), r.namespace)))
347}
348
349/// The name and the schema of the type with this OID.
350pub(crate) fn type_name(oid: u32, session: &dyn Session) -> Option<(&str, u32)> {
351    builtin::type_by_oid(oid)
352        .map(|row| (row.name, row.namespace))
353        .or_else(|| session.catalog()?.type_by_oid(oid).map(|t| (t.name.as_str(), t.namespace)))
354        .or_else(|| row_type_relation(oid, session).map(|r| (r.name.as_str(), r.namespace)))
355}
356
357/// The relation of the user whose row type is `ty`, which has the name and the schema of the relation.
358fn row_type_relation(ty: u32, session: &dyn Session) -> Option<&rupg_catalog::Relation> {
359    session.catalog()?.relations().find(|r| ty != 0 && r.row_type == ty)
360}
361
362/// `RelationIsVisible`, or `None` when no relation has the OID.
363pub(crate) fn class_visible(oid: u32, session: &dyn Session) -> Option<bool> {
364    let (name, _) = class_name(oid, session)?;
365    Some(find_class(&temp_path(session), name, session) == Some(oid))
366}
367
368/// `TypeIsVisible`, or `None` when no type has the OID.
369pub(crate) fn type_visible(oid: u32, session: &dyn Session) -> Option<bool> {
370    let (name, _) = type_name(oid, session)?;
371    Some(find_type(&temp_path(session), name, session) == Some(oid))
372}
373
374/// `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.
375fn functions(spaces: &[u32], name: &str, nargs: Option<usize>) -> Vec<&'static ProcRow> {
376    let mut found: Vec<&'static ProcRow> = Vec::new();
377    for &ns in spaces {
378        for proc in builtin::procs_named(name) {
379            if proc.namespace == ns
380                && nargs.is_none_or(|n| proc.argtypes.len() == n)
381                && !found.iter().any(|f| f.argtypes == proc.argtypes)
382            {
383                found.push(proc);
384            }
385        }
386    }
387    found
388}
389
390/// `OpernameGetCandidates`: the operators with the name in the schemas, in the order of the schemas. An operator with the same argument types as an operator in a schema before it is hidden.
391fn operators(spaces: &[u32], name: &str) -> Vec<&'static OperatorRow> {
392    let mut found: Vec<&'static OperatorRow> = Vec::new();
393    for &ns in spaces {
394        for op in builtin::operators_named(name) {
395            if op.namespace == ns && !found.iter().any(|f| (f.left, f.right) == (op.left, op.right))
396            {
397                found.push(op);
398            }
399        }
400    }
401    found
402}
403
404/// `FunctionIsVisible`.
405pub(crate) fn function_visible(proc: &ProcRow, session: &dyn Session) -> bool {
406    functions(&path(session), proc.name, Some(proc.argtypes.len()))
407        .iter()
408        .find(|f| f.argtypes == proc.argtypes)
409        .is_some_and(|f| f.oid == proc.oid)
410}
411
412/// `OperatorIsVisible`.
413pub(crate) fn operator_visible(op: &OperatorRow, session: &dyn Session) -> bool {
414    operators(&path(session), op.name)
415        .iter()
416        .find(|f| (f.left, f.right) == (op.left, op.right))
417        .is_some_and(|f| f.oid == op.oid)
418}
419
420/// `F_ARRAY_SUBSCRIPT_HANDLER`, the `typsubscript` of a true array type.
421const ARRAY_SUBSCRIPT_HANDLER: u32 = 6179;
422/// The bytes of the header of a `varlena` value, which a typmod of a string type counts.
423const VARHDRSZ: i32 = 4;
424
425/// `format_type_extended` with `FORMAT_TYPE_ALLOW_INVALID`: `-` for OID 0, or the name of a type, with its schema when the name alone does not find it, and with its typmod. A typmod of `None` is the form without `FORMAT_TYPE_TYPEMOD_GIVEN`.
426///
427/// # Errors
428///
429/// `0A000` for a typmod of `interval`, which the engine cannot show yet.
430pub(crate) fn type_text(ty: u32, typmod: Option<i32>, session: &dyn Session) -> Result<String> {
431    if ty == 0 {
432        return Ok("-".to_string());
433    }
434    format_type(ty, typmod, false, session)
435}
436
437/// `format_type_be_qualified` with `FORMAT_TYPE_INVALID_AS_NULL`: the name of a type with its schema, also when the name is visible, or `None` when no type has the OID. A type with a special name of SQL, such as `integer`, has no schema.
438pub(crate) fn type_text_qualified(ty: u32, session: &dyn Session) -> Option<String> {
439    type_name(ty, session)?;
440    format_type(ty, None, true, session).ok().filter(|text| !text.starts_with("???"))
441}
442
443/// `format_type_extended`: the name of a type, with its schema when `qualify` is true or when the name alone does not find it.
444fn format_type(
445    ty: u32,
446    typmod: Option<i32>,
447    qualify: bool,
448    session: &dyn Session,
449) -> Result<String> {
450    // A true array type shows the name of its element type with `[]`, and a plain array type such as `oidvector` shows its own name.
451    let element = match builtin::type_by_oid(ty) {
452        Some(row) if row.elem != 0 && row.subscript == ARRAY_SUBSCRIPT_HANDLER => {
453            (row.storage != b'p').then_some(row.elem)
454        }
455        Some(_) => None,
456        None => match session.catalog().and_then(|c| c.type_by_oid(ty)) {
457            Some(user) => (user.element != 0).then_some(user.element),
458            None if row_type_relation(ty, session).is_some() => None,
459            None => return Ok("???".to_string()),
460        },
461    };
462    match element {
463        Some(element) if type_name(element, session).is_none() => Ok("???[]".to_string()),
464        Some(element) => Ok(format!("{}[]", base_text(element, typmod, qualify, session)?)),
465        None => base_text(ty, typmod, qualify, session),
466    }
467}
468
469/// The part of `format_type_extended` after the array test: the name of a type that is not a true array type.
470fn base_text(ty: u32, typmod: Option<i32>, qualify: bool, session: &dyn Session) -> Result<String> {
471    let given = typmod.is_some();
472    let with = typmod.filter(|m| *m >= 0);
473    let special = match (ty, with) {
474        (oid::BIT, Some(m)) => Some(format!("bit({m})")),
475        (oid::BIT, None) if !given => Some("bit".to_string()),
476        (oid::BOOL, _) => Some("boolean".to_string()),
477        (oid::BPCHAR, Some(m)) => Some(format!("character{}", length(m))),
478        (oid::BPCHAR, None) if !given => Some("character".to_string()),
479        (oid::FLOAT4, _) => Some("real".to_string()),
480        (oid::FLOAT8, _) => Some("double precision".to_string()),
481        (oid::INT2, _) => Some("smallint".to_string()),
482        (oid::INT4, _) => Some("integer".to_string()),
483        (oid::INT8, _) => Some("bigint".to_string()),
484        (oid::NUMERIC, Some(m)) if m >= VARHDRSZ => {
485            let bits = m - VARHDRSZ;
486            let scale = ((bits & 0x7ff) ^ 1024) - 1024;
487            Some(format!("numeric({},{scale})", (bits >> 16) & 0xffff))
488        }
489        (oid::NUMERIC, _) => Some("numeric".to_string()),
490        (oid::INTERVAL, Some(m)) => Some(format!("interval{}", interval_typmod(m)?)),
491        (oid::INTERVAL, None) => Some("interval".to_string()),
492        (oid::TIME, m) => Some(format!("time{} without time zone", precision(m))),
493        (oid::TIMETZ, m) => Some(format!("time{} with time zone", precision(m))),
494        (oid::TIMESTAMP, m) => Some(format!("timestamp{} without time zone", precision(m))),
495        (oid::TIMESTAMPTZ, m) => Some(format!("timestamp{} with time zone", precision(m))),
496        (oid::VARBIT, m) => Some(format!("bit varying{}", precision(m))),
497        (oid::VARCHAR, m) => Some(format!("character varying{}", m.map_or(String::new(), length))),
498        (oid::JSON, _) => Some("json".to_string()),
499        _ => None,
500    };
501    if let Some(text) = special {
502        return Ok(text);
503    }
504    let Some((name, namespace)) = type_name(ty, session) else { return Ok("???".to_string()) };
505    let schema = if !qualify && type_visible(ty, session) == Some(true) {
506        None
507    } else {
508        namespace_name(namespace, session)
509    };
510    let text = qualified(schema, name);
511    // The types with a `typmodout` function all have a special form, so the default form shows the typmod as a number.
512    Ok(match with {
513        Some(m) => format!("{text}({m})"),
514        None => text,
515    })
516}
517
518/// The typmod text of `anytime_typmodout` and `bittypmodout`.
519fn precision(typmod: Option<i32>) -> String {
520    typmod.map_or(String::new(), |m| format!("({m})"))
521}
522
523/// The typmod text of `intervaltypmodout`: the fields of the range, then the precision when the typmod has one.
524fn interval_typmod(typmod: i32) -> Result<String> {
525    const YEAR: i32 = 1 << 2;
526    const MONTH: i32 = 1 << 1;
527    const DAY: i32 = 1 << 3;
528    const HOUR: i32 = 1 << 10;
529    const MINUTE: i32 = 1 << 11;
530    const SECOND: i32 = 1 << 12;
531    let (precision, range) = types::typmod::interval_precision_range(typmod).unwrap_or_default();
532    let fields = match range {
533        YEAR => " year",
534        MONTH => " month",
535        DAY => " day",
536        HOUR => " hour",
537        MINUTE => " minute",
538        SECOND => " second",
539        r if r == YEAR | MONTH => " year to month",
540        r if r == DAY | HOUR => " day to hour",
541        r if r == DAY | HOUR | MINUTE => " day to minute",
542        r if r == DAY | HOUR | MINUTE | SECOND => " day to second",
543        r if r == HOUR | MINUTE => " hour to minute",
544        r if r == HOUR | MINUTE | SECOND => " hour to second",
545        r if r == MINUTE | SECOND => " minute to second",
546        types::typmod::INTERVAL_FULL_RANGE => "",
547        _ => {
548            return Err(Error::new(
549                SqlState::INTERNAL_ERROR,
550                format!("invalid INTERVAL typmod: 0x{typmod:x}"),
551            ));
552        }
553    };
554    Ok(if precision == types::typmod::INTERVAL_FULL_PRECISION {
555        fields.to_string()
556    } else {
557        format!("{fields}({precision})")
558    })
559}
560
561/// The typmod text of `bpchartypmodout` and `varchartypmodout`.
562fn length(typmod: i32) -> String {
563    if typmod > VARHDRSZ { format!("({})", typmod - VARHDRSZ) } else { String::new() }
564}
565
566/// The text output of an OID alias type: the name of the object, with its schema when the name alone does not find it, the text of OID 0 for the kind, or the number for an OID that no object has.
567pub(crate) fn output(kind: RegKind, oid: u32, session: &dyn Session, out: &mut Vec<u8>) {
568    let text = if oid == 0 { None } else { name_of(kind, oid, session) };
569    match text {
570        Some(text) => out.extend_from_slice(text.as_bytes()),
571        None => types::reg_out_oid(kind, oid, out),
572    }
573}
574
575/// The text of the object with this OID, or `None` when no object has it.
576pub(crate) fn name_of(kind: RegKind, oid: u32, session: &dyn Session) -> Option<String> {
577    match kind {
578        RegKind::Class => {
579            let (name, namespace) = class_name(oid, session)?;
580            let schema = if class_visible(oid, session)? {
581                None
582            } else {
583                namespace_name(namespace, session)
584            };
585            Some(qualified(schema, name))
586        }
587        RegKind::Collation => catalog_text(Named::Collation, oid, session),
588        RegKind::Config => catalog_text(Named::Config, oid, session),
589        RegKind::Dictionary => catalog_text(Named::Dictionary, oid, session),
590        RegKind::Namespace => namespace_name(oid, session).map(quote_identifier),
591        RegKind::Role => rupg_pgcatalog::roles::current().name(oid).map(quote_identifier),
592        RegKind::Database => match builtin::named_by_oid(Named::Database, oid) {
593            Some(row) => Some(quote_identifier(row.name)),
594            None => {
595                session.databases().iter().find(|d| d.oid == oid).map(|d| quote_identifier(&d.name))
596            }
597        },
598        RegKind::Type => {
599            type_name(oid, session)?;
600            Some(type_text(oid, None, session).ok()?)
601        }
602        RegKind::Proc => {
603            let proc = builtin::proc_by_oid(oid)?;
604            let only =
605                matches!(functions(&path(session), proc.name, None)[..], [f] if f.oid == oid);
606            let schema = if only { None } else { namespace_name(proc.namespace, session) };
607            Some(qualified(schema, proc.name))
608        }
609        RegKind::Procedure => {
610            let proc = builtin::proc_by_oid(oid)?;
611            let schema = if function_visible(proc, session) {
612                None
613            } else {
614                namespace_name(proc.namespace, session)
615            };
616            let args: Vec<_> = proc
617                .argtypes
618                .iter()
619                .map(|&ty| type_text(ty, None, session).unwrap_or_else(|_| "???".to_string()))
620                .collect();
621            Some(format!("{}({})", qualified(schema, proc.name), args.join(",")))
622        }
623        RegKind::Oper => {
624            let op = builtin::operator_by_oid(oid)?;
625            let only = matches!(operators(&path(session), op.name)[..], [f] if f.oid == oid);
626            match namespace_name(op.namespace, session) {
627                Some(schema) if !only => Some(format!("{}.{}", quote_identifier(schema), op.name)),
628                _ => Some(op.name.to_string()),
629            }
630        }
631        RegKind::Operator => {
632            let op = builtin::operator_by_oid(oid)?;
633            let mut text = String::new();
634            if !operator_visible(op, session)
635                && let Some(schema) = namespace_name(op.namespace, session)
636            {
637                text.push_str(&quote_identifier(schema));
638                text.push('.');
639            }
640            let arg =
641                |ty: u32| if ty == 0 { Cow::Borrowed("NONE") } else { types::format_type(ty) };
642            Some(format!("{text}{}({},{})", op.name, arg(op.left), arg(op.right)))
643        }
644    }
645}
646
647/// The text input of an OID alias type: a number, `-` for OID 0, or the name of an object.
648///
649/// # Errors
650///
651/// The outer error is an error for any caller, such as a name with too many dots or a syntax error in a type name. The inner error is an error of the input function that `to_regclass` and the other functions give as a null, such as a name that does not exist.
652pub(crate) fn input(kind: RegKind, text: &str, session: &dyn Session) -> Soft<u32> {
653    match types::reg_in(kind, text) {
654        Ok(RegInput::Oid(oid)) => return Ok(Ok(oid)),
655        Ok(RegInput::Name(_)) => {}
656        Err(e) => return Ok(Err(type_error(e))),
657    }
658    match kind {
659        RegKind::Type => {
660            let env = SessionEnv(session);
661            Ok(rupg_analyze::parse_type(text, &env)?.map(|(ty, _)| ty))
662        }
663        RegKind::Procedure => procedure_in(text, session),
664        RegKind::Operator => operator_in(text, session),
665        _ => {
666            let names = match qualified_name_list(text) {
667                Ok(names) => names,
668                Err(e) => return Ok(Err(type_error(e))),
669            };
670            name_in(kind, text, &names, session)
671        }
672    }
673}
674
675/// The input of a kind whose name is a list of names.
676fn name_in(kind: RegKind, text: &str, names: &[Cow<'_, str>], session: &dyn Session) -> Soft<u32> {
677    let list = name_list(names);
678    let simple = |named: &[(&str, u32)], state: SqlState, what: &str| match names {
679        [name] => match named.iter().find(|n| n.0 == name) {
680            Some(n) => Ok(Ok(n.1)),
681            None => soft(state, format!("{what} \"{name}\" does not exist")),
682        },
683        _ => soft(SqlState::INVALID_NAME, "invalid name syntax"),
684    };
685    let rows = |named: Named| -> Vec<(&str, u32)> {
686        builtin::named(named).iter().map(|r| (r.name, r.oid)).collect()
687    };
688    let mut schemas = rows(Named::Namespace);
689    schemas.retain(|&(_, oid)| !schema_dropped(oid, session));
690    if let Some(catalog) = session.catalog() {
691        schemas.extend(catalog.schemas().map(|s| (s.name.as_str(), s.oid)));
692    }
693    let catalog_in = |named: Named, state: SqlState, message: String| {
694        let (schema, name) = deconstruct(names, session)?;
695        match catalog_find(named, &spaces(schema, session), name, session) {
696            Some(oid) => Ok(Ok(oid)),
697            None => soft(state, message),
698        }
699    };
700    match kind {
701        RegKind::Class => class_in(names, session, false),
702        RegKind::Namespace => simple(&schemas, SqlState::UNDEFINED_SCHEMA, "schema"),
703        RegKind::Role => {
704            let roles = rupg_pgcatalog::roles::current();
705            simple(&roles.all().collect::<Vec<_>>(), SqlState::UNDEFINED_OBJECT, "role")
706        }
707        RegKind::Database => {
708            let created = session.databases();
709            let mut databases = rows(Named::Database);
710            databases.extend(created.iter().map(|d| (d.name.as_str(), d.oid)));
711            simple(&databases, SqlState::UNDEFINED_OBJECT, "database")
712        }
713        RegKind::Collation => catalog_in(
714            Named::Collation,
715            SqlState::UNDEFINED_OBJECT,
716            format!("collation \"{list}\" for encoding \"{UTF8_NAME}\" does not exist"),
717        ),
718        RegKind::Config => catalog_in(
719            Named::Config,
720            SqlState::UNDEFINED_OBJECT,
721            format!("text search configuration \"{list}\" does not exist"),
722        ),
723        RegKind::Dictionary => catalog_in(
724            Named::Dictionary,
725            SqlState::UNDEFINED_OBJECT,
726            format!("text search dictionary \"{list}\" does not exist"),
727        ),
728        RegKind::Proc => {
729            let (schema, name) = deconstruct(names, session)?;
730            match functions(&spaces(schema, session), name, None)[..] {
731                [] => soft(
732                    SqlState::UNDEFINED_FUNCTION,
733                    format!("function \"{text}\" does not exist"),
734                ),
735                [proc] => Ok(Ok(proc.oid)),
736                _ => soft(
737                    SqlState::AMBIGUOUS_FUNCTION,
738                    format!("more than one function named \"{text}\""),
739                ),
740            }
741        }
742        RegKind::Oper => {
743            let (schema, name) = deconstruct(names, session)?;
744            match operators(&spaces(schema, session), name)[..] {
745                [] => {
746                    soft(SqlState::UNDEFINED_FUNCTION, format!("operator does not exist: {text}"))
747                }
748                [op] => Ok(Ok(op.oid)),
749                _ => soft(
750                    SqlState::AMBIGUOUS_FUNCTION,
751                    format!("more than one operator named {text}"),
752                ),
753            }
754        }
755        RegKind::Type | RegKind::Procedure | RegKind::Operator => {
756            Err(Error::internal("a type name read as a list of names"))
757        }
758    }
759}
760
761/// `makeRangeVarFromNameList` and `RangeVarGetRelid`: the relation of a list of names. With `strict`, as the cast from `text` gives, a relation or a schema that does not exist is a hard error with the message of `RangeVarGetRelid`.
762fn class_in(names: &[Cow<'_, str>], session: &dyn Session, strict: bool) -> Soft<u32> {
763    let (schema, name) = match names {
764        [name] => (None, name),
765        [schema, name] => (Some(schema), name),
766        [database, schema, name] => {
767            if database != session.database() {
768                return Err(Error::new(
769                    SqlState::FEATURE_NOT_SUPPORTED,
770                    format!(
771                        "cross-database references are not implemented: \"{database}.{schema}.{name}\""
772                    ),
773                ));
774            }
775            (Some(schema), name)
776        }
777        _ => {
778            return Err(Error::new(
779                SqlState::SYNTAX_ERROR,
780                format!("improper relation name (too many dotted names): {}", name_list(names)),
781            ));
782        }
783    };
784    let spaces = match schema {
785        Some(schema) => match namespace_oid(schema, session) {
786            Some(ns) => vec![ns],
787            None if strict => {
788                return Err(Error::new(
789                    SqlState::UNDEFINED_SCHEMA,
790                    format!("schema \"{schema}\" does not exist"),
791                ));
792            }
793            None => Vec::new(),
794        },
795        None => temp_path(session),
796    };
797    if let Some(oid) = find_class(&spaces, name, session) {
798        return Ok(Ok(oid));
799    }
800    let message = if strict {
801        match schema {
802            Some(schema) => format!("relation \"{schema}.{name}\" does not exist"),
803            None => format!("relation \"{name}\" does not exist"),
804        }
805    } else {
806        format!("relation \"{}\" does not exist", name_list(names))
807    };
808    let error = Error::new(SqlState::UNDEFINED_TABLE, message);
809    if strict { Err(error) } else { Ok(Err(error)) }
810}
811
812/// The bytes that `scanner_isspace` takes as white space.
813fn is_space(b: u8) -> bool {
814    matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
815}
816
817/// The error `22P02` of `parseNameAndArgTypes`.
818fn bad_syntax<T>(message: &str) -> Soft<T> {
819    soft(SqlState::INVALID_TEXT_REPRESENTATION, message)
820}
821
822/// The name and the argument types of a function or an operator.
823type NameAndArgs<'a> = (Vec<Cow<'a, str>>, Vec<u32>);
824
825/// `parseNameAndArgTypes`: the name and the argument types of a string such as `abs(integer)`. With `allow_none`, `NONE` in any case is the missing argument of a prefix operator, which is 0.
826fn name_and_args<'a>(
827    text: &'a str,
828    allow_none: bool,
829    session: &dyn Session,
830) -> Soft<NameAndArgs<'a>> {
831    let b = text.as_bytes();
832    let mut in_quote = false;
833    let mut open = None;
834    for (i, &c) in b.iter().enumerate() {
835        if c == b'"' {
836            in_quote = !in_quote;
837        } else if c == b'(' && !in_quote {
838            open = Some(i);
839            break;
840        }
841    }
842    let Some(open) = open else { return bad_syntax("expected a left parenthesis") };
843    let names = match qualified_name_list(&text[..open]) {
844        Ok(names) => names,
845        Err(e) => return Ok(Err(type_error(e))),
846    };
847    let rest = &text[open + 1..];
848    let rb = rest.as_bytes();
849    // The backward scan of the C code stops at the first byte after the parenthesis, so that byte is the end when all the bytes after it are white space.
850    let close = (1..rb.len()).rev().find(|&i| !is_space(rb[i])).unwrap_or(0);
851    if rb.get(close) != Some(&b')') {
852        return bad_syntax("expected a right parenthesis");
853    }
854    let args = &rest[..close];
855    let ab = args.as_bytes();
856    let env = SessionEnv(session);
857    let mut types = Vec::new();
858    let mut at = 0;
859    let mut had_comma = false;
860    loop {
861        while at < ab.len() && is_space(ab[at]) {
862            at += 1;
863        }
864        if at == ab.len() {
865            if had_comma {
866                return bad_syntax("expected a type name");
867            }
868            break;
869        }
870        let start = at;
871        let mut in_quote = false;
872        let mut depth = 0i32;
873        while at < ab.len() {
874            match ab[at] {
875                b'"' => in_quote = !in_quote,
876                b',' if !in_quote && depth == 0 => break,
877                b'(' | b'[' if !in_quote => depth += 1,
878                b')' | b']' if !in_quote => depth -= 1,
879                _ => {}
880            }
881            at += 1;
882        }
883        if in_quote || depth != 0 {
884            return bad_syntax("improper type name");
885        }
886        let name = args[start..at].trim_end_matches(|c: char| u8::try_from(c).is_ok_and(is_space));
887        had_comma = at < ab.len();
888        if had_comma {
889            at += 1;
890        }
891        let ty = if allow_none && name.eq_ignore_ascii_case("none") {
892            0
893        } else {
894            match rupg_analyze::parse_type(name, &env)? {
895                Ok((ty, _)) => ty,
896                Err(e) => return Ok(Err(e)),
897            }
898        };
899        if types.len() >= FUNC_MAX_ARGS {
900            return soft(SqlState::TOO_MANY_ARGUMENTS, "too many arguments");
901        }
902        types.push(ty);
903    }
904    Ok(Ok((names, types)))
905}
906
907/// `regprocedurein`.
908fn procedure_in(text: &str, session: &dyn Session) -> Soft<u32> {
909    let (names, args) = match name_and_args(text, false, session)? {
910        Ok(found) => found,
911        Err(e) => return Ok(Err(e)),
912    };
913    let (schema, name) = deconstruct(&names, session)?;
914    match functions(&spaces(schema, session), name, Some(args.len()))
915        .iter()
916        .find(|f| f.argtypes == args.as_slice())
917    {
918        Some(proc) => Ok(Ok(proc.oid)),
919        None => soft(SqlState::UNDEFINED_FUNCTION, format!("function \"{text}\" does not exist")),
920    }
921}
922
923/// `regoperatorin`.
924fn operator_in(text: &str, session: &dyn Session) -> Soft<u32> {
925    let (names, args) = match name_and_args(text, true, session)? {
926        Ok(found) => found,
927        Err(e) => return Ok(Err(e)),
928    };
929    let [left, right] = args[..] else {
930        let error = if args.len() == 1 {
931            Error::new(SqlState::UNDEFINED_PARAMETER, "missing argument")
932                .with_hint("Use NONE to denote the missing argument of a unary operator.")
933        } else {
934            Error::new(SqlState::TOO_MANY_ARGUMENTS, "too many arguments")
935                .with_hint("Provide two argument types for operator.")
936        };
937        return Ok(Err(error));
938    };
939    let (schema, name) = deconstruct(&names, session)?;
940    let found = spaces(schema, session).into_iter().find_map(|ns| {
941        builtin::operators_named(name)
942            .find(|op| op.namespace == ns && op.left == left && op.right == right)
943    });
944    match found {
945        Some(op) => Ok(Ok(op.oid)),
946        None => soft(SqlState::UNDEFINED_FUNCTION, format!("operator does not exist: {text}")),
947    }
948}
949
950/// The text argument of a function.
951pub(crate) fn text_arg(args: &[Value]) -> Result<&str> {
952    args.first().and_then(Value::as_str).ok_or_else(bad_value)
953}
954
955/// `to_regclass` and the other functions: the input of the type, or null for a soft error.
956fn to_reg(kind: RegKind, call: &Call<'_>, args: &[Value]) -> Result<Value> {
957    Ok(input(kind, text_arg(args)?, call.session)?.map_or(Value::Null, Value::Oid))
958}
959
960/// `to_regtypemod`: the typmod of a type name, or null for a type that does not exist.
961fn to_regtypemod(call: &Call<'_>, args: &[Value]) -> Result<Value> {
962    let env = SessionEnv(call.session);
963    let found = rupg_analyze::parse_type(text_arg(args)?, &env)?;
964    Ok(found.map_or(Value::Null, |(_, typmod)| Value::Int4(typmod)))
965}
966
967/// `pg_input_validate`: the soft error of the input of the first argument as the type that the second argument names, or `None` for a good value. A type name that is not good and a hard error of the input are errors. For a domain, the value must also pass the check constraints.
968fn input_error(call: &Call<'_>, args: &[Value]) -> Result<Option<Error>> {
969    let text = text_arg(args)?;
970    let name = args.get(1).and_then(Value::as_str).ok_or_else(bad_value)?;
971    let env = SessionEnv(call.session);
972    let (ty, typmod) = rupg_analyze::parse_type(name, &env)??;
973    let value = match crate::io::input_soft(ty, text, typmod, call.session)? {
974        Ok(value) => value,
975        Err(error) => return Ok(Some(error)),
976    };
977    let element = builtin::type_by_oid(ty).map_or(0, |row| row.elem);
978    if builtin::domain_by_oid(ty).is_some() || builtin::domain_by_oid(element).is_some() {
979        return call.session.input_checks(ty, &value);
980    }
981    Ok(None)
982}
983
984/// `pg_input_is_valid`: true when the text is a good value of the type.
985fn input_is_valid(call: &Call<'_>, args: &[Value]) -> Result<Value> {
986    Ok(Value::Bool(input_error(call, args)?.is_none()))
987}
988
989/// `pg_input_error_info`: the message, the detail, the hint and the SQLSTATE of the soft error of the input, or a row of nulls for a good value.
990fn input_error_info(call: &Call<'_>, args: &[Value]) -> Result<Value> {
991    let values = match input_error(call, args)? {
992        None => vec![Value::Null; 4],
993        Some(error) => {
994            let text = |s: Option<&str>| s.map_or(Value::Null, Value::text);
995            vec![
996                Value::text(error.message()),
997                text(error.detail()),
998                text(error.hint()),
999                Value::text(error.state().as_str()),
1000            ]
1001        }
1002    };
1003    Ok(Value::Record(Box::new(Record::new(vec![oid::TEXT; 4], values))))
1004}
1005
1006/// The relation of a list of names, as `RangeVarGetRelid` finds it for `text_regclass`.
1007///
1008/// # Errors
1009///
1010/// `42P01` for a relation that does not exist, `3F000` for a schema that does not exist, and the errors of a list of names that is not correct.
1011pub(crate) fn relation_oid(names: &[Cow<'_, str>], session: &dyn Session) -> Result<u32> {
1012    class_in(names, session, true)?
1013}
1014
1015/// `text_regclass`, the cast from `text` to `regclass`. A name that does not exist is an error, as `RangeVarGetRelid` gives it.
1016fn text_regclass(call: &Call<'_>, args: &[Value]) -> Result<Value> {
1017    let names = qualified_name_list(text_arg(args)?).map_err(type_error)?;
1018    Ok(Value::Oid(relation_oid(&names, call.session)?))
1019}
1020
1021/// The kernel of a function of this module by its `prosrc`.
1022pub(crate) fn by_src(src: &str) -> Option<Kernel> {
1023    let kernel: Kernel = match src {
1024        "to_regclass" => |c, a| to_reg(RegKind::Class, c, a),
1025        "to_regcollation" => |c, a| to_reg(RegKind::Collation, c, a),
1026        "to_regdatabase" => |c, a| to_reg(RegKind::Database, c, a),
1027        "to_regnamespace" => |c, a| to_reg(RegKind::Namespace, c, a),
1028        "to_regoper" => |c, a| to_reg(RegKind::Oper, c, a),
1029        "to_regoperator" => |c, a| to_reg(RegKind::Operator, c, a),
1030        "to_regproc" => |c, a| to_reg(RegKind::Proc, c, a),
1031        "to_regprocedure" => |c, a| to_reg(RegKind::Procedure, c, a),
1032        "to_regrole" => |c, a| to_reg(RegKind::Role, c, a),
1033        "to_regtype" => |c, a| to_reg(RegKind::Type, c, a),
1034        "to_regtypemod" => to_regtypemod,
1035        "pg_input_is_valid" => input_is_valid,
1036        "pg_input_error_info" => input_error_info,
1037        "text_regclass" => text_regclass,
1038        _ => return None,
1039    };
1040    Some(kernel)
1041}