Skip to main content

rupg_types/
reg.rs

1//! The OID alias types, such as `regclass` and `regtype`, and the name lists that their input reads.
2//!
3//! A port of the parts of `src/backend/utils/adt/regproc.c` that do not read the catalog, and of `SplitIdentifierString` in `varlena.c`. A value of each type is an OID. The input takes a number or a name, and the output gives the name of the object or the number when no object has the OID. The catalog finds the names, so here [`reg_in`] gives the number or the name to look up, and [`reg_out_oid`] writes the text when the catalog has no name. The binary form of each type is the binary form of `oid`.
4//!
5//! Lifted from `crates/rudb-pgtypes/src/reg.rs` of tamnd/rudb at f5f7065a (spec/04 section 4.9).
6
7use std::borrow::Cow;
8
9use rupg_common::SqlState;
10
11use crate::error::TypeError;
12use crate::generated::oids;
13use crate::number::{is_space, oid_in};
14use crate::scalar::name_in;
15use crate::types::Oid;
16
17/// One of the OID alias types.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum RegKind {
20    /// `regproc`, a function by name.
21    Proc,
22    /// `regprocedure`, a function with its argument types.
23    Procedure,
24    /// `regoper`, an operator by name.
25    Oper,
26    /// `regoperator`, an operator with its argument types.
27    Operator,
28    /// `regclass`, a relation.
29    Class,
30    /// `regtype`, a type.
31    Type,
32    /// `regrole`, a role.
33    Role,
34    /// `regnamespace`, a schema.
35    Namespace,
36    /// `regcollation`, a collation.
37    Collation,
38    /// `regconfig`, a text search configuration.
39    Config,
40    /// `regdictionary`, a text search dictionary.
41    Dictionary,
42    /// `regdatabase`, a database.
43    Database,
44}
45
46impl RegKind {
47    /// All the kinds, in the order of their OIDs in `pg_type.dat`.
48    pub const ALL: [RegKind; 12] = [
49        RegKind::Proc,
50        RegKind::Procedure,
51        RegKind::Oper,
52        RegKind::Operator,
53        RegKind::Class,
54        RegKind::Type,
55        RegKind::Config,
56        RegKind::Dictionary,
57        RegKind::Namespace,
58        RegKind::Role,
59        RegKind::Collation,
60        RegKind::Database,
61    ];
62
63    /// The kind of a type OID, or `None` when the type is not an OID alias type.
64    pub fn from_oid(oid: Oid) -> Option<RegKind> {
65        RegKind::ALL.into_iter().find(|kind| kind.oid() == oid)
66    }
67
68    /// The OID of the type.
69    pub fn oid(self) -> Oid {
70        match self {
71            RegKind::Proc => oids::REGPROC,
72            RegKind::Procedure => oids::REGPROCEDURE,
73            RegKind::Oper => oids::REGOPER,
74            RegKind::Operator => oids::REGOPERATOR,
75            RegKind::Class => oids::REGCLASS,
76            RegKind::Type => oids::REGTYPE,
77            RegKind::Role => oids::REGROLE,
78            RegKind::Namespace => oids::REGNAMESPACE,
79            RegKind::Collation => oids::REGCOLLATION,
80            RegKind::Config => oids::REGCONFIG,
81            RegKind::Dictionary => oids::REGDICTIONARY,
82            RegKind::Database => oids::REGDATABASE,
83        }
84    }
85
86    /// The name of the type, such as `regclass`.
87    pub fn type_name(self) -> &'static str {
88        match self {
89            RegKind::Proc => "regproc",
90            RegKind::Procedure => "regprocedure",
91            RegKind::Oper => "regoper",
92            RegKind::Operator => "regoperator",
93            RegKind::Class => "regclass",
94            RegKind::Type => "regtype",
95            RegKind::Role => "regrole",
96            RegKind::Namespace => "regnamespace",
97            RegKind::Collation => "regcollation",
98            RegKind::Config => "regconfig",
99            RegKind::Dictionary => "regdictionary",
100            RegKind::Database => "regdatabase",
101        }
102    }
103
104    /// The text of OID 0. It is `-`, but the operator types use `0`, because `-` is the name of an operator.
105    pub fn zero_text(self) -> &'static str {
106        match self {
107            RegKind::Oper | RegKind::Operator => "0",
108            _ => "-",
109        }
110    }
111}
112
113/// What the text input of an OID alias type found.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum RegInput<'a> {
116    /// The input is a number, or `-` for OID 0. The value needs no lookup.
117    Oid(u32),
118    /// The input is a name. The catalog finds the OID, with the errors of the kind.
119    Name(&'a str),
120}
121
122/// The first step of the text input of an OID alias type, `parseDashOrOid` and `parseNumericOid`.
123///
124/// A string of only ASCII digits is an OID, read as `oid` reads it. So `010` is octal and is 8, and `09` is an error of the type `oid`. `-` is OID 0, but not for `regoper` and `regoperator`. Any other string is a name, which the catalog reads with [`qualified_name_list`] or the type parser.
125pub fn reg_in(kind: RegKind, s: &str) -> Result<RegInput<'_>, TypeError> {
126    if s == "-" && kind.zero_text() == "-" {
127        return Ok(RegInput::Oid(0));
128    }
129    if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
130        return oid_in(s).map(RegInput::Oid);
131    }
132    Ok(RegInput::Name(s))
133}
134
135/// The text output of an OID alias type when the catalog has no object with the OID: the text of OID 0 for the kind, or the number.
136pub fn reg_out_oid(kind: RegKind, oid: u32, out: &mut Vec<u8>) {
137    if oid == 0 {
138        out.extend_from_slice(kind.zero_text().as_bytes());
139    } else {
140        crate::number::oid_out(oid, out);
141    }
142}
143
144/// A name in a list of identifiers. An unquoted name is in lowercase. A quoted name keeps its case and has one quote for each pair of quotes. Both are cut to 63 bytes.
145fn identifier(token: &str, quoted: bool) -> Cow<'_, str> {
146    let name: Cow<'_, str> = if quoted {
147        if token.contains("\"\"") {
148            Cow::Owned(token.replace("\"\"", "\""))
149        } else {
150            Cow::Borrowed(token)
151        }
152    } else if token.bytes().any(|b| b.is_ascii_uppercase()) {
153        // Only ASCII letters change, as in `downcase_identifier` for a multibyte encoding.
154        Cow::Owned(token.to_ascii_lowercase())
155    } else {
156        Cow::Borrowed(token)
157    };
158    match name {
159        Cow::Borrowed(s) => Cow::Borrowed(name_in(s)),
160        Cow::Owned(mut s) => {
161            s.truncate(name_in(&s).len());
162            Cow::Owned(s)
163        }
164    }
165}
166
167/// `SplitIdentifierString`: the identifiers in `s` with `separator` between them, or `None` when the syntax is not correct.
168///
169/// White space can be before and after each identifier. An identifier in double quotes can have any character, and two quotes in it are one quote. An identifier without quotes ends at the separator or at white space, and it cannot be empty. A string of only white space is an empty list. The search path setting and the name input of the OID alias types use this syntax.
170pub fn split_identifier_string(s: &str, separator: u8) -> Option<Vec<Cow<'_, str>>> {
171    let b = s.as_bytes();
172    let skip_space = |mut at: usize| {
173        while at < b.len() && is_space(b[at]) {
174            at += 1;
175        }
176        at
177    };
178    let mut names = Vec::new();
179    let mut at = skip_space(0);
180    if at == b.len() {
181        return Some(names);
182    }
183    loop {
184        if b[at] == b'"' {
185            // The end is the first quote that is not one of a pair.
186            let start = at + 1;
187            let mut end = start;
188            loop {
189                end += b[end..].iter().position(|&c| c == b'"')?;
190                if b.get(end + 1) != Some(&b'"') {
191                    break;
192                }
193                end += 2;
194            }
195            names.push(identifier(&s[start..end], true));
196            at = end + 1;
197        } else {
198            let start = at;
199            while at < b.len() && b[at] != separator && !is_space(b[at]) {
200                at += 1;
201            }
202            if at == start {
203                return None;
204            }
205            names.push(identifier(&s[start..at], false));
206        }
207        at = skip_space(at);
208        if at == b.len() {
209            return Some(names);
210        }
211        if b[at] != separator {
212            return None;
213        }
214        at = skip_space(at + 1);
215        if at == b.len() {
216            // A separator at the end needs one more name, and an empty name is an error.
217            return None;
218        }
219    }
220}
221
222/// `stringToQualifiedNameList`: the parts of a name such as `schema.table`, with `42602` and `invalid name syntax` when the syntax is not correct or there is no name.
223pub fn qualified_name_list(s: &str) -> Result<Vec<Cow<'_, str>>, TypeError> {
224    match split_identifier_string(s, b'.') {
225        Some(names) if !names.is_empty() => Ok(names),
226        _ => Err(TypeError::new(SqlState::INVALID_NAME, "invalid name syntax".to_owned())),
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn the_input_takes_a_number_or_a_dash() {
236        assert_eq!(reg_in(RegKind::Class, "-"), Ok(RegInput::Oid(0)));
237        assert_eq!(reg_in(RegKind::Oper, "-"), Ok(RegInput::Name("-")));
238        assert_eq!(reg_in(RegKind::Type, "010"), Ok(RegInput::Oid(8)));
239        assert_eq!(reg_in(RegKind::Type, "4294967295"), Ok(RegInput::Oid(u32::MAX)));
240        assert_eq!(reg_in(RegKind::Type, " 1"), Ok(RegInput::Name(" 1")));
241        assert_eq!(reg_in(RegKind::Type, ""), Ok(RegInput::Name("")));
242        let error = reg_in(RegKind::Class, "09").unwrap_err();
243        assert_eq!(error.message, "invalid input syntax for type oid: \"09\"");
244        let error = reg_in(RegKind::Class, "4294967296").unwrap_err();
245        assert_eq!(error.message, "value \"4294967296\" is out of range for type oid");
246    }
247
248    #[test]
249    fn the_output_of_zero_depends_on_the_kind() {
250        let text = |kind, oid| {
251            let mut out = Vec::new();
252            reg_out_oid(kind, oid, &mut out);
253            String::from_utf8(out).unwrap()
254        };
255        assert_eq!(text(RegKind::Class, 0), "-");
256        assert_eq!(text(RegKind::Operator, 0), "0");
257        assert_eq!(text(RegKind::Type, 70000), "70000");
258        for kind in RegKind::ALL {
259            assert_eq!(RegKind::from_oid(kind.oid()), Some(kind));
260        }
261        assert_eq!(RegKind::from_oid(oids::OID), None);
262    }
263
264    #[test]
265    fn name_lists_split_at_the_separator() {
266        let list = |s| qualified_name_list(s).map(|names| names.join("|"));
267        assert_eq!(list("Ab . \"C\"\"d\""), Ok("ab|C\"d".to_owned()));
268        assert_eq!(list("\"\""), Ok(String::new()));
269        assert_eq!(list("a\"b.c"), Ok("a\"b|c".to_owned()));
270        assert_eq!(list("ÉA"), Ok("Éa".to_owned()));
271        for bad in ["", "  ", "a.", ".a", "a..b", "a b", "\"a", "\"a\"b", "a.\"b\"\""] {
272            assert_eq!(list(bad).unwrap_err().message, "invalid name syntax", "{bad:?}");
273        }
274        let long = format!("{}é", "a".repeat(62));
275        assert_eq!(list(&long), Ok("a".repeat(62)));
276        assert_eq!(split_identifier_string(" a , B ", b',').unwrap(), ["a", "b"]);
277    }
278}