Skip to main content

rudb_functions/
typecatalog.rs

1//! What `duckdb_types()` says about each type name this engine knows.
2//!
3//! One entry per name, and one row per entry per modifier signature, which is why 73 names produce 93
4//! rows. The list, the oids, the modifier signatures and the row order were all read off the pinned
5//! binary rather than worked out from first principles, because every one of them turned out to have
6//! something in it that reading the type system would not have told you.
7//!
8//! # The table lists the types this engine has
9//!
10//! The pinned binary returns 104 rows in the `memory` schema and this returns 93. The eleven that are
11//! not here are ten names for types rudb does not have at all, `array`, `bignum`, `enum`,
12//! `geometry`, `timestamptz_ns`, `time_ns`, `tuple`, `type`, `variant` and `varint`, plus `geometry`
13//! a second time for its `crs` modifier. A catalog table that listed a type you cannot make a value
14//! of would be a table that lies, and the point of this one is that a client can read it to find out
15//! what the engine supports. The names come back when the types do.
16//!
17//! `list` is here even though `NULL::LIST(INTEGER)` is a parser error in both engines. The name is a
18//! catalog entry rather than something a cast can spell, the spelling that works is `INTEGER[]`, and
19//! rudb has had a list vector since #302, so the row belongs here for the same reason it is there.
20//!
21//! # A name is not a type and the modifiers are per name
22//!
23//! `timestamp` takes a `precision` modifier and `timestamp_us` does not, and they are the same type.
24//! `varchar` takes `length` and `collation` and `blob` takes neither, and both are variable length
25//! bytes. So the signatures are stored per name and not derived from the type, which was the first
26//! guess and is wrong six ways.
27//!
28//! What is derived is the size and the category, because those really are properties of the type.
29//! `type_size` comes from [`rudb_common::PhysicalType::size`], which is where the three
30//! divergences from the pin are written down, and the category is the function below.
31//!
32//! # `type_oid` is on one row out of however many a type has
33//!
34//! The oids are `LogicalTypeId`, which is a stable enumeration in someone else's public header, so
35//! reproducing them is meaningful in a way that reproducing a catalog's allocation counter is not.
36//! A type has one oid and several names, and the pin puts the oid on the alphabetically first of
37//! those names, on that name's bare signature row, and leaves it null everywhere else. So `bigint`
38//! carries 14 and `int8`, `int64`, `long` and `oid` carry nothing, and `bpchar` carries 25 while
39//! `varchar` carries nothing. That is measured, not guessed, and `the_oid_sits_on_the_first_name_of_its_type`
40//! re-derives it from the entries so a new alias cannot silently take an oid off the name that had it.
41//!
42//! # Row order
43//!
44//! Reproduced, unlike `duckdb_keywords()`, because it is a rule rather than an implementation detail.
45//! Names sort case insensitively with `_` ranked after the letters, so `timestamptz_ns` comes before
46//! `timestamp_ms` and `timetz` before `time_ns`, and a space sorts first, so `time with time zone`
47//! comes directly after `time`. Within one name the signatures are in declaration order, which is why
48//! `bpchar` is bare then `length` then `collation` rather than in alphabetical order.
49
50use rudb_common::{Field, LogicalType};
51
52/// One modifier signature: the parameter names and the type each one takes.
53pub type Signature = &'static [(&'static str, &'static str)];
54
55/// One type name in the catalog, and everything `duckdb_types()` says about it.
56#[derive(Debug, Clone, Copy)]
57pub struct TypeEntry {
58    /// The name, as the pin spells it, which is lower case for everything rudb has.
59    pub name: &'static str,
60    /// The canonical type this name means, as the pin writes it in the `logical_type` column.
61    pub logical_type: &'static str,
62    /// The `LogicalTypeId` of the type, on the one name that carries it and `None` on the rest.
63    pub oid: Option<i64>,
64    /// The type the trailing variadic argument takes, for the names that have one.
65    pub varargs: Option<&'static str>,
66    /// One per row this name produces, in the order the pin returns them.
67    pub signatures: &'static [Signature],
68}
69
70/// No modifiers, which is what most names have and all a few of them have.
71const BARE: &[Signature] = &[&[]];
72
73/// The three a string name takes, in the pin's order.
74const STRING: &[Signature] = &[&[], &[("length", "BIGINT")], &[("collation", "VARCHAR")]];
75
76/// Bare or a length, which is the bit string pair.
77const LENGTH: &[Signature] = &[&[], &[("length", "BIGINT")]];
78
79/// Bare or a width and a scale, which is the decimal pair.
80const WIDTH_SCALE: &[Signature] = &[&[], &[("width", "UTINYINT"), ("scale", "UTINYINT")]];
81
82/// Bare or a precision, which is what the two names that take one have.
83const PRECISION: &[Signature] = &[&[], &[("precision", "UTINYINT")]];
84
85/// Every type name, in the order `duckdb_types()` returns them.
86///
87/// See the module documentation for why the order is what it is, why the list is shorter than the
88/// pin's, and why the oid is on the name it is on.
89pub static TYPE_NAMES: &[TypeEntry] = &[
90    entry("bigint", "BIGINT", Some(14)),
91    entry("binary", "BLOB", Some(26)),
92    TypeEntry { signatures: LENGTH, oid: Some(36), ..entry("bit", "BIT", None) },
93    TypeEntry { signatures: LENGTH, ..entry("bitstring", "BIT", None) },
94    entry("blob", "BLOB", None),
95    entry("bool", "BOOLEAN", Some(10)),
96    entry("boolean", "BOOLEAN", None),
97    TypeEntry { signatures: STRING, oid: Some(25), ..entry("bpchar", "VARCHAR", None) },
98    entry("bytea", "BLOB", None),
99    TypeEntry { signatures: STRING, ..entry("char", "VARCHAR", None) },
100    entry("date", "DATE", Some(15)),
101    TypeEntry { signatures: PRECISION, oid: Some(19), ..entry("datetime", "TIMESTAMP", None) },
102    TypeEntry { signatures: WIDTH_SCALE, oid: Some(21), ..entry("dec", "DECIMAL", None) },
103    TypeEntry { signatures: WIDTH_SCALE, ..entry("decimal", "DECIMAL", None) },
104    entry("double", "DOUBLE", Some(23)),
105    entry("float", "FLOAT", Some(22)),
106    entry("float4", "FLOAT", None),
107    entry("float8", "DOUBLE", None),
108    entry("guid", "UUID", Some(54)),
109    entry("hugeint", "HUGEINT", Some(50)),
110    entry("int", "INTEGER", Some(13)),
111    entry("int1", "TINYINT", Some(11)),
112    entry("int128", "HUGEINT", None),
113    entry("int16", "SMALLINT", Some(12)),
114    entry("int2", "SMALLINT", None),
115    entry("int32", "INTEGER", None),
116    entry("int4", "INTEGER", None),
117    entry("int64", "BIGINT", None),
118    entry("int8", "BIGINT", None),
119    entry("integer", "INTEGER", None),
120    entry("integral", "INTEGER", None),
121    TypeEntry { signatures: PRECISION, oid: Some(27), ..entry("interval", "INTERVAL", None) },
122    TypeEntry {
123        signatures: &[&[("child", "TYPE")]],
124        oid: Some(101),
125        ..entry("list", "LIST", None)
126    },
127    entry("logical", "BOOLEAN", None),
128    entry("long", "BIGINT", None),
129    TypeEntry {
130        signatures: &[&[("key", "TYPE"), ("value", "TYPE")]],
131        oid: Some(102),
132        ..entry("map", "MAP", None)
133    },
134    entry("null", "NULL", Some(1)),
135    TypeEntry { signatures: WIDTH_SCALE, ..entry("numeric", "DECIMAL", None) },
136    TypeEntry { signatures: STRING, ..entry("nvarchar", "VARCHAR", None) },
137    entry("oid", "BIGINT", None),
138    entry("real", "FLOAT", None),
139    TypeEntry { varargs: Some("TYPE"), oid: Some(100), ..entry("row", "STRUCT", None) },
140    entry("short", "SMALLINT", None),
141    entry("signed", "INTEGER", None),
142    entry("smallint", "SMALLINT", None),
143    TypeEntry { signatures: STRING, ..entry("string", "VARCHAR", None) },
144    TypeEntry { varargs: Some("TYPE"), ..entry("struct", "STRUCT", None) },
145    TypeEntry { signatures: STRING, ..entry("text", "VARCHAR", None) },
146    entry("time", "TIME", Some(16)),
147    entry("time with time zone", "TIME WITH TIME ZONE", Some(34)),
148    TypeEntry { signatures: PRECISION, ..entry("timestamp", "TIMESTAMP", None) },
149    entry("timestamp with time zone", "TIMESTAMP WITH TIME ZONE", Some(32)),
150    entry("timestamptz", "TIMESTAMP WITH TIME ZONE", None),
151    entry("timestamp_ms", "TIMESTAMP_MS", Some(18)),
152    entry("timestamp_ns", "TIMESTAMP_NS", Some(20)),
153    entry("timestamp_s", "TIMESTAMP_S", Some(17)),
154    entry("timestamp_us", "TIMESTAMP", None),
155    entry("timetz", "TIME WITH TIME ZONE", None),
156    entry("tinyint", "TINYINT", None),
157    entry("ubigint", "UBIGINT", Some(31)),
158    entry("uhugeint", "UHUGEINT", Some(49)),
159    entry("uint128", "UHUGEINT", None),
160    entry("uint16", "USMALLINT", Some(29)),
161    entry("uint32", "UINTEGER", Some(30)),
162    entry("uint64", "UBIGINT", None),
163    entry("uint8", "UTINYINT", Some(28)),
164    entry("uinteger", "UINTEGER", None),
165    TypeEntry { varargs: Some("TYPE"), oid: Some(107), ..entry("union", "UNION", None) },
166    entry("usmallint", "USMALLINT", None),
167    entry("utinyint", "UTINYINT", None),
168    entry("uuid", "UUID", None),
169    entry("varbinary", "BLOB", None),
170    TypeEntry { signatures: STRING, ..entry("varchar", "VARCHAR", None) },
171];
172
173/// One bare name, which is the shape most of the table is, so the rest can be written as a change to
174/// it rather than as seventy repetitions of the same four fields.
175const fn entry(name: &'static str, logical_type: &'static str, oid: Option<i64>) -> TypeEntry {
176    TypeEntry { name, logical_type, oid, varargs: None, signatures: BARE }
177}
178
179/// The columns `duckdb_types()` produces, which is DuckDB's seventeen.
180#[must_use]
181pub fn type_fields() -> Vec<Field> {
182    vec![
183        Field::new("database_name", LogicalType::Varchar),
184        Field::new("database_oid", LogicalType::BigInt),
185        Field::new("schema_name", LogicalType::Varchar),
186        Field::new("schema_oid", LogicalType::BigInt),
187        Field::new("type_oid", LogicalType::BigInt),
188        Field::new("type_name", LogicalType::Varchar),
189        Field::new("type_size", LogicalType::BigInt),
190        Field::new("logical_type", LogicalType::Varchar),
191        Field::new("type_category", LogicalType::Varchar),
192        Field::new("comment", LogicalType::Varchar),
193        Field::new("tags", LogicalType::map(LogicalType::Varchar, LogicalType::Varchar)),
194        Field::new("internal", LogicalType::Boolean),
195        Field::new("extension_name", LogicalType::Varchar),
196        Field::new("labels", LogicalType::list(LogicalType::Varchar)),
197        Field::new("parameters", LogicalType::list(LogicalType::Varchar)),
198        Field::new("parameter_types", LogicalType::list(LogicalType::Varchar)),
199        Field::new("varargs", LogicalType::Varchar),
200    ]
201}
202
203/// The representative type a canonical name stands for, and `None` for a name that is a family
204/// rather than a type.
205///
206/// A `DECIMAL` has no one width and a `LIST` has no one element, so the argument is whatever makes
207/// the answer to the two questions this is asked right: the layout and the category. A decimal of
208/// any width is `NUMERIC` and is stored in an integer whose size depends on the width, which is why
209/// the size is reported null and the category is not.
210#[must_use]
211pub fn representative(logical_type: &str) -> Option<LogicalType> {
212    Some(match logical_type {
213        "NULL" => LogicalType::Null,
214        "BOOLEAN" => LogicalType::Boolean,
215        "TINYINT" => LogicalType::TinyInt,
216        "SMALLINT" => LogicalType::SmallInt,
217        "INTEGER" => LogicalType::Integer,
218        "BIGINT" => LogicalType::BigInt,
219        "HUGEINT" => LogicalType::HugeInt,
220        "UTINYINT" => LogicalType::UTinyInt,
221        "USMALLINT" => LogicalType::USmallInt,
222        "UINTEGER" => LogicalType::UInteger,
223        "UBIGINT" => LogicalType::UBigInt,
224        "UHUGEINT" => LogicalType::UHugeInt,
225        "FLOAT" => LogicalType::Float,
226        "DOUBLE" => LogicalType::Double,
227        "DECIMAL" => LogicalType::Decimal { width: 18, scale: 3 },
228        "VARCHAR" => LogicalType::Varchar,
229        "BLOB" => LogicalType::Blob,
230        "BIT" => LogicalType::Bit,
231        "UUID" => LogicalType::Uuid,
232        "DATE" => LogicalType::Date,
233        "TIME" => LogicalType::Time,
234        "TIME WITH TIME ZONE" => LogicalType::TimeTz,
235        "TIMESTAMP" => LogicalType::Timestamp,
236        "TIMESTAMP_S" => LogicalType::TimestampS,
237        "TIMESTAMP_MS" => LogicalType::TimestampMs,
238        "TIMESTAMP_NS" => LogicalType::TimestampNs,
239        "TIMESTAMP WITH TIME ZONE" => LogicalType::TimestampTz,
240        "INTERVAL" => LogicalType::Interval,
241        "LIST" => LogicalType::list(LogicalType::Integer),
242        "MAP" => LogicalType::map(LogicalType::Varchar, LogicalType::Varchar),
243        "STRUCT" => LogicalType::Struct(Vec::new()),
244        "UNION" => LogicalType::Union(Vec::new()),
245        _ => return None,
246    })
247}
248
249/// The `LogicalTypeId` of a canonical type name, and `None` for one this catalog does not carry.
250///
251/// The same number [`TypeEntry::oid`] holds, read the other way round. That column puts the oid on
252/// the alphabetically first name of a type and leaves it null on the aliases, because that is what
253/// the pin does, so finding a type's oid means scanning for the one entry that has it rather than
254/// looking up a name. `duckdb_columns()` reports this as `data_type_id` and does not care which name
255/// somebody wrote the column with, so `INTEGER` and `int4` both come out as 13.
256#[must_use]
257pub fn type_oid(logical_type: &str) -> Option<i64> {
258    TYPE_NAMES
259        .iter()
260        .find(|entry| entry.logical_type == logical_type && entry.oid.is_some())
261        .and_then(|entry| entry.oid)
262}
263
264/// How many bytes one value of this name takes, and `None` for the one name where it depends.
265///
266/// A decimal is stored in the narrowest integer that holds its width, so there is no answer until
267/// somebody says how wide. The pinned binary reports null for the same reason.
268#[must_use]
269pub fn type_size(logical_type: &str) -> Option<i64> {
270    if logical_type == "DECIMAL" {
271        return None;
272    }
273    let ty = representative(logical_type)?;
274    i64::try_from(ty.physical().size()).ok()
275}
276
277/// Which of DuckDB's categories a type is in, and `None` for the ones it puts in none.
278///
279/// Six categories and a gap. `BIT`, `BLOB`, `UUID` and the null type are in no category at all,
280/// which is not an oversight anybody can fix from here, it is what the pin reports and this table is
281/// checked against the pin.
282#[must_use]
283pub fn type_category(logical_type: &str) -> Option<&'static str> {
284    let ty = representative(logical_type)?;
285    Some(match ty {
286        LogicalType::Boolean => "BOOLEAN",
287        LogicalType::Varchar => "STRING",
288        LogicalType::TinyInt
289        | LogicalType::SmallInt
290        | LogicalType::Integer
291        | LogicalType::BigInt
292        | LogicalType::HugeInt
293        | LogicalType::UTinyInt
294        | LogicalType::USmallInt
295        | LogicalType::UInteger
296        | LogicalType::UBigInt
297        | LogicalType::UHugeInt
298        | LogicalType::Float
299        | LogicalType::Double
300        | LogicalType::Decimal { .. } => "NUMERIC",
301        LogicalType::Date
302        | LogicalType::Time
303        | LogicalType::TimeTz
304        | LogicalType::Timestamp
305        | LogicalType::TimestampS
306        | LogicalType::TimestampMs
307        | LogicalType::TimestampNs
308        | LogicalType::TimestampTz
309        | LogicalType::Interval => "DATETIME",
310        LogicalType::List(_)
311        | LogicalType::Array(_, _)
312        | LogicalType::Map(_, _)
313        | LogicalType::Struct(_)
314        | LogicalType::Union(_) => "COMPOSITE",
315        _ => return None,
316    })
317}
318
319/// The sort key a type name is ordered by, which is not the name.
320///
321/// Case insensitive, and `_` ranked after the letters rather than before them, which is how the pin
322/// puts `timestamptz_ns` before `timestamp_ms` and `timetz` before `time_ns`. A space is left alone
323/// and so sorts first, which is how `time with time zone` lands directly after `time`.
324#[must_use]
325pub fn sort_key(name: &str) -> String {
326    name.to_ascii_lowercase().replace('_', "{")
327}
328
329#[cfg(test)]
330mod tests {
331    use rudb_common::LogicalType;
332
333    use super::{TYPE_NAMES, sort_key, type_category, type_fields, type_size};
334
335    #[test]
336    fn the_table_is_the_shape_the_pin_returns() {
337        let rows: usize = TYPE_NAMES.iter().map(|entry| entry.signatures.len()).sum();
338        assert_eq!(TYPE_NAMES.len(), 73, "names");
339        assert_eq!(rows, 93, "rows, which is the pin's 104 less the eleven for types we lack");
340        assert_eq!(type_fields().len(), 17);
341    }
342
343    /// The rule the pin follows, re-derived here rather than trusted, so that adding an alias that
344    /// sorts before the name currently carrying an oid fails instead of producing two oids or none.
345    #[test]
346    fn the_oid_sits_on_the_first_name_of_its_type() {
347        for entry in TYPE_NAMES {
348            let first = TYPE_NAMES
349                .iter()
350                .filter(|other| other.logical_type == entry.logical_type)
351                .min_by_key(|other| sort_key(other.name))
352                .expect("at least itself");
353            let expected = entry.name == first.name;
354            assert_eq!(
355                entry.oid.is_some(),
356                expected,
357                "{} carries an oid and {} is the first name of {}",
358                entry.name,
359                first.name,
360                entry.logical_type
361            );
362        }
363        // Every type has exactly one oid and no two types share one.
364        let mut oids: Vec<i64> = TYPE_NAMES.iter().filter_map(|entry| entry.oid).collect();
365        oids.sort_unstable();
366        let total = oids.len();
367        oids.dedup();
368        assert_eq!(oids.len(), total, "two names claim the same oid");
369        assert_eq!(total, 32, "one oid per type this engine has");
370    }
371
372    #[test]
373    fn the_names_are_in_the_order_the_pin_returns_them() {
374        let mut sorted: Vec<&str> = TYPE_NAMES.iter().map(|entry| entry.name).collect();
375        sorted.sort_by_key(|name| sort_key(name));
376        let listed: Vec<&str> = TYPE_NAMES.iter().map(|entry| entry.name).collect();
377        assert_eq!(listed, sorted);
378        // The three pairs that say the collation is not the byte order.
379        assert!(sort_key("timestamptz_ns") < sort_key("timestamp_ms"));
380        assert!(sort_key("timetz") < sort_key("time_ns"));
381        assert!(sort_key("time with time zone") < sort_key("timestamp"));
382    }
383
384    /// Every name in this table has to be a name the type parser accepts, or the table is advertising
385    /// something a query cannot use. `list` is the one exception and the module says why.
386    #[test]
387    fn every_name_here_is_a_name_a_cast_can_spell() {
388        for entry in TYPE_NAMES {
389            if entry.name == "list" {
390                assert!(LogicalType::parse("list").is_err(), "the pin refuses this too");
391                continue;
392            }
393            let spelled = match entry.logical_type {
394                "DECIMAL" => format!("{}(9, 2)", entry.name),
395                "MAP" => format!("{}(VARCHAR, VARCHAR)", entry.name),
396                "STRUCT" | "UNION" => format!("{}(a INTEGER)", entry.name),
397                _ => entry.name.to_string(),
398            };
399            let parsed = LogicalType::parse(&spelled)
400                .unwrap_or_else(|e| panic!("{} does not parse: {e}", entry.name));
401            let canonical = entry.logical_type.to_string();
402            // The null type prints with quotes round it, which is DuckDB's spelling in a `typeof` and
403            // not its spelling in this column, so the two really do differ by the quotes and nothing
404            // else. The parser accepts both, which is why a quoted name is a type name at all.
405            let got = parsed.to_string().replace('"', "");
406            assert!(
407                got == canonical || got.starts_with(&canonical),
408                "{} parses to {got} and the table says {canonical}",
409                entry.name
410            );
411        }
412    }
413
414    /// The three numbers that are this engine's layout rather than the pin's, kept as an assertion so
415    /// that a change to either one is a decision somebody makes.
416    #[test]
417    fn the_sizes_are_this_engines_and_three_of_them_differ_from_the_pin() {
418        assert_eq!(type_size("INTEGER"), Some(4));
419        assert_eq!(type_size("VARCHAR"), Some(16));
420        assert_eq!(type_size("HUGEINT"), Some(16));
421        assert_eq!(type_size("DECIMAL"), None, "the width decides, so there is no one answer");
422        assert_eq!(type_size("STRUCT"), Some(0), "the parent holds nothing of its own");
423        // Two u32 here and two u64 there.
424        assert_eq!(type_size("LIST"), Some(8), "the pin says 16");
425        assert_eq!(type_size("MAP"), Some(8), "the pin says 16");
426        // Nothing stored at all here, an INT32 body there.
427        assert_eq!(type_size("NULL"), Some(0), "the pin says 4");
428    }
429
430    #[test]
431    fn four_types_are_in_no_category_and_that_is_the_pins_answer() {
432        assert_eq!(type_category("BIGINT"), Some("NUMERIC"));
433        assert_eq!(type_category("DECIMAL"), Some("NUMERIC"));
434        assert_eq!(type_category("VARCHAR"), Some("STRING"));
435        assert_eq!(type_category("BOOLEAN"), Some("BOOLEAN"));
436        assert_eq!(type_category("INTERVAL"), Some("DATETIME"));
437        assert_eq!(type_category("MAP"), Some("COMPOSITE"));
438        for uncategorised in ["NULL", "BIT", "BLOB", "UUID"] {
439            assert_eq!(type_category(uncategorised), None, "{uncategorised}");
440        }
441    }
442
443    /// The signatures are per name and not per type, which is the thing about this table that looks
444    /// like a mistake until you check it against the pin.
445    #[test]
446    fn two_names_for_one_type_can_take_different_modifiers() {
447        let of = |name: &str| {
448            TYPE_NAMES.iter().find(|entry| entry.name == name).expect("a name in the table")
449        };
450        assert_eq!(of("timestamp").signatures.len(), 2, "timestamp takes a precision");
451        assert_eq!(of("timestamp_us").signatures.len(), 1, "the same type, and it does not");
452        assert_eq!(of("varchar").signatures.len(), 3);
453        assert_eq!(of("blob").signatures.len(), 1);
454        // Declaration order and not alphabetical, which is what the pin returns.
455        assert_eq!(of("bpchar").signatures[1], [("length", "BIGINT")]);
456        assert_eq!(of("bpchar").signatures[2], [("collation", "VARCHAR")]);
457    }
458}