Skip to main content

rudb_functions/
entrycatalog.rs

1//! The columns of the tables that describe what somebody created.
2//!
3//! `duckdb_databases()`, `duckdb_schemas()`, `duckdb_tables()`, `duckdb_views()` and
4//! `duckdb_columns()`. Only the columns are here, because unlike the other metadata tables the rows
5//! are not a fact about the binary. They are whatever is in the catalog, so they are built in
6//! `rudb_exec` where the catalog is in reach and this crate is only the half both ends agree on.
7//!
8//! # Every one of these tables has an oid column and rudb fills them in
9//!
10//! `duckdb_types()` reports `database_oid` as null and says so in its own doc, because upstream's is
11//! a counter its catalog handed out at startup and reproducing an accident of one process's ordering
12//! is not compatibility. These tables are the other case. A tool reads `duckdb_columns()` and joins
13//! it to `duckdb_tables()` on `table_oid`, and a null there is not a small divergence, it is the
14//! table failing at the one job it has. So the catalog hands out its own oids and these report them.
15//! The numbers will not be upstream's and they are not meant to be. What has to hold is that the
16//! same entry carries the same number in every table that names it, and that no number is handed out
17//! twice.
18//!
19//! # What rudb has fewer of
20//!
21//! Upstream returns three databases on a fresh in memory session, `memory`, `system` and `temp`, and
22//! five schemas. rudb has `memory.main` and nothing else, so it returns one of each. `system` holds
23//! the builtins and `temp` holds what `CREATE TEMP TABLE` makes, and rudb has neither an attached
24//! catalog for its builtins nor a temporary one. `duckdb_functions()` already reports `system.main`
25//! for every function it lists, which is a name `duckdb_schemas()` does not return, and that
26//! disagreement is real rather than an oversight here. It is filed rather than papered over.
27
28use rudb_common::{Field, LogicalType};
29
30/// The `type` column of `duckdb_databases()`, which says what is behind an attached name.
31pub const DUCKDB: &str = "duckdb";
32
33/// The columns `duckdb_databases()` returns, in the pin's order.
34#[must_use]
35pub fn database_fields() -> Vec<Field> {
36    vec![
37        Field::new("database_name", LogicalType::Varchar),
38        Field::new("database_oid", LogicalType::BigInt),
39        Field::new("path", LogicalType::Varchar),
40        Field::new("comment", LogicalType::Varchar),
41        Field::new("tags", tags()),
42        Field::new("internal", LogicalType::Boolean),
43        Field::new("type", LogicalType::Varchar),
44        Field::new("readonly", LogicalType::Boolean),
45        Field::new("encrypted", LogicalType::Boolean),
46        Field::new("cipher", LogicalType::Varchar),
47        Field::new("options", tags()),
48    ]
49}
50
51/// The columns `duckdb_schemas()` returns, in the pin's order.
52///
53/// `oid` first and unqualified, which is this table alone. Every other one spells the column after
54/// what it names, so a query that reads several of them has to remember that the schema's own oid is
55/// `oid` here and `schema_oid` everywhere else.
56#[must_use]
57pub fn schema_fields() -> Vec<Field> {
58    vec![
59        Field::new("oid", LogicalType::BigInt),
60        Field::new("database_name", LogicalType::Varchar),
61        Field::new("database_oid", LogicalType::BigInt),
62        Field::new("schema_name", LogicalType::Varchar),
63        Field::new("comment", LogicalType::Varchar),
64        Field::new("tags", tags()),
65        Field::new("internal", LogicalType::Boolean),
66        Field::new("sql", LogicalType::Varchar),
67        Field::new("parent_schema", LogicalType::Varchar),
68        Field::new("parent_schema_oid", LogicalType::BigInt),
69    ]
70}
71
72/// The columns `duckdb_tables()` returns, in the pin's order.
73#[must_use]
74pub fn table_fields() -> Vec<Field> {
75    vec![
76        Field::new("database_name", LogicalType::Varchar),
77        Field::new("database_oid", LogicalType::BigInt),
78        Field::new("schema_name", LogicalType::Varchar),
79        Field::new("schema_oid", LogicalType::BigInt),
80        Field::new("table_name", LogicalType::Varchar),
81        Field::new("table_oid", LogicalType::BigInt),
82        Field::new("comment", LogicalType::Varchar),
83        Field::new("tags", tags()),
84        Field::new("internal", LogicalType::Boolean),
85        Field::new("temporary", LogicalType::Boolean),
86        Field::new("has_primary_key", LogicalType::Boolean),
87        Field::new("estimated_size", LogicalType::BigInt),
88        Field::new("column_count", LogicalType::BigInt),
89        Field::new("index_count", LogicalType::BigInt),
90        Field::new("check_constraint_count", LogicalType::BigInt),
91        Field::new("sql", LogicalType::Varchar),
92    ]
93}
94
95/// The columns `duckdb_views()` returns, in the pin's order.
96///
97/// Thirteen, which is three fewer than `duckdb_tables()` and not the same thirteen. There is no
98/// `estimated_size` and no `index_count` because a view holds nothing, and there is an `is_bound`
99/// which says whether the column cache on the entry holds anything yet.
100#[must_use]
101pub fn view_fields() -> Vec<Field> {
102    vec![
103        Field::new("database_name", LogicalType::Varchar),
104        Field::new("database_oid", LogicalType::BigInt),
105        Field::new("schema_name", LogicalType::Varchar),
106        Field::new("schema_oid", LogicalType::BigInt),
107        Field::new("view_name", LogicalType::Varchar),
108        Field::new("view_oid", LogicalType::BigInt),
109        Field::new("comment", LogicalType::Varchar),
110        Field::new("tags", tags()),
111        Field::new("internal", LogicalType::Boolean),
112        Field::new("temporary", LogicalType::Boolean),
113        Field::new("column_count", LogicalType::BigInt),
114        Field::new("sql", LogicalType::Varchar),
115        Field::new("is_bound", LogicalType::Boolean),
116    ]
117}
118
119/// The columns `duckdb_columns()` returns, in the pin's order.
120#[must_use]
121pub fn column_fields() -> Vec<Field> {
122    vec![
123        Field::new("database_name", LogicalType::Varchar),
124        Field::new("database_oid", LogicalType::BigInt),
125        Field::new("schema_name", LogicalType::Varchar),
126        Field::new("schema_oid", LogicalType::BigInt),
127        Field::new("table_name", LogicalType::Varchar),
128        Field::new("table_oid", LogicalType::BigInt),
129        Field::new("column_name", LogicalType::Varchar),
130        Field::new("column_index", LogicalType::Integer),
131        Field::new("comment", LogicalType::Varchar),
132        Field::new("internal", LogicalType::Boolean),
133        Field::new("column_default", LogicalType::Varchar),
134        Field::new("is_nullable", LogicalType::Boolean),
135        Field::new("data_type", LogicalType::Varchar),
136        Field::new("data_type_id", LogicalType::BigInt),
137        Field::new("character_maximum_length", LogicalType::Integer),
138        Field::new("numeric_precision", LogicalType::Integer),
139        Field::new("numeric_precision_radix", LogicalType::Integer),
140        Field::new("numeric_scale", LogicalType::Integer),
141        Field::new("tags", tags()),
142        Field::new("is_generated", LogicalType::Boolean),
143        Field::new("generation_expression", LogicalType::Varchar),
144    ]
145}
146
147/// The canonical name of a type, which is what [`crate::typecatalog::type_oid`] is keyed by.
148///
149/// The type written out, minus whatever modifiers it carries. `DECIMAL(9,2)` and `DECIMAL(38,10)`
150/// are both the same type as far as `data_type_id` is concerned, and so are a list of integers and a
151/// list of strings, because the oid is `LogicalTypeId` and that enumeration has one entry for the
152/// type constructor rather than one per instance of it.
153#[must_use]
154pub fn canonical(ty: &LogicalType) -> String {
155    match ty {
156        LogicalType::Decimal { .. } => "DECIMAL".to_string(),
157        LogicalType::List(_) | LogicalType::Array(_, _) => "LIST".to_string(),
158        LogicalType::Map(_, _) => "MAP".to_string(),
159        LogicalType::Struct(_) => "STRUCT".to_string(),
160        LogicalType::Union(_) => "UNION".to_string(),
161        other => other.to_string(),
162    }
163}
164
165/// The three numeric columns of `duckdb_columns()`, which most types report nothing in.
166///
167/// Measured off the pin rather than reasoned about, because the answers are not what the column
168/// names suggest. `numeric_precision` on an integer is a count of bits and not of digits, so an
169/// `INTEGER` reports 32 with a radix of 2, and a `FLOAT` reports 24 and a `DOUBLE` 53 because those
170/// are the mantissa widths. A `DECIMAL` is the one type where the number means digits, so it reports
171/// its width with a radix of 10 and its scale. Every unsigned integer reports nothing at all, which
172/// looks like an oversight upstream and is reproduced here because a client reading these is reading
173/// them from DuckDB's side of the comparison.
174#[must_use]
175pub fn numeric_facts(ty: &LogicalType) -> (Option<i32>, Option<i32>, Option<i32>) {
176    let binary = |bits| (Some(bits), Some(2), Some(0));
177    match ty {
178        LogicalType::TinyInt => binary(8),
179        LogicalType::SmallInt => binary(16),
180        LogicalType::Integer => binary(32),
181        LogicalType::BigInt => binary(64),
182        LogicalType::HugeInt => binary(128),
183        LogicalType::Float => binary(24),
184        LogicalType::Double => binary(53),
185        LogicalType::Decimal { width, scale } => {
186            (Some(i32::from(*width)), Some(10), Some(i32::from(*scale)))
187        }
188        _ => (None, None, None),
189    }
190}
191
192/// The `MAP(VARCHAR, VARCHAR)` that every one of these tables carries at least one of.
193fn tags() -> LogicalType {
194    LogicalType::map(LogicalType::Varchar, LogicalType::Varchar)
195}
196
197#[cfg(test)]
198mod tests {
199    use rudb_common::LogicalType;
200
201    use super::{
202        canonical, column_fields, database_fields, numeric_facts, schema_fields, table_fields,
203        view_fields,
204    };
205
206    #[test]
207    fn the_five_tables_are_the_shape_the_pin_returns() {
208        assert_eq!(database_fields().len(), 11);
209        assert_eq!(schema_fields().len(), 10);
210        assert_eq!(table_fields().len(), 16);
211        assert_eq!(view_fields().len(), 13);
212        assert_eq!(column_fields().len(), 21);
213    }
214
215    /// The four values read off the pin, which are not the ones the column names suggest.
216    #[test]
217    fn a_numeric_precision_is_bits_everywhere_except_on_a_decimal() {
218        assert_eq!(numeric_facts(&LogicalType::Integer), (Some(32), Some(2), Some(0)));
219        assert_eq!(numeric_facts(&LogicalType::Double), (Some(53), Some(2), Some(0)));
220        assert_eq!(
221            numeric_facts(&LogicalType::Decimal { width: 9, scale: 2 }),
222            (Some(9), Some(10), Some(2))
223        );
224        // An unsigned integer reports nothing, which is upstream's answer and not an omission here.
225        assert_eq!(numeric_facts(&LogicalType::UBigInt), (None, None, None));
226        assert_eq!(numeric_facts(&LogicalType::Varchar), (None, None, None));
227    }
228
229    #[test]
230    fn a_types_modifiers_are_not_part_of_the_name_the_oid_is_keyed_by() {
231        assert_eq!(canonical(&LogicalType::Decimal { width: 9, scale: 2 }), "DECIMAL");
232        assert_eq!(canonical(&LogicalType::list(LogicalType::Integer)), "LIST");
233        assert_eq!(canonical(&LogicalType::Integer), "INTEGER");
234        assert_eq!(canonical(&LogicalType::TimestampTz), "TIMESTAMP WITH TIME ZONE");
235    }
236
237    /// The one column name that does not follow the rule the other four tables follow.
238    #[test]
239    fn a_schemas_own_oid_is_spelled_oid_and_not_schema_oid() {
240        assert_eq!(schema_fields()[0].name, "oid");
241        assert!(schema_fields().iter().all(|field| field.name != "schema_oid"));
242    }
243}