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 one column `PRAGMA show_tables` returns.
120///
121/// Named `name` and nothing else, because the statement answers what is in reach of an unqualified
122/// name and a client that wants to know where each one lives asks `PRAGMA show_tables_expanded`.
123#[must_use]
124pub fn show_table_fields() -> Vec<Field> {
125    vec![Field::new("name", LogicalType::Varchar)]
126}
127
128/// The one column `PRAGMA show_databases` returns.
129///
130/// `database_name` rather than `name`, which is the spelling `duckdb_databases()` uses as well, and
131/// it differs from the column `PRAGMA show_tables` returns for no reason either of them states.
132#[must_use]
133pub fn show_database_fields() -> Vec<Field> {
134    vec![Field::new("database_name", LogicalType::Varchar)]
135}
136
137/// The six columns `PRAGMA show_tables_expanded` returns.
138///
139/// The column names and the column types are two `VARCHAR[]` down one row rather than a row each,
140/// which makes this the one catalog table that answers a table's shape without a join. `temporary`
141/// is the last column and it is the only one that is not a name.
142#[must_use]
143pub fn show_expanded_fields() -> Vec<Field> {
144    vec![
145        Field::new("database", LogicalType::Varchar),
146        Field::new("schema", LogicalType::Varchar),
147        Field::new("name", LogicalType::Varchar),
148        Field::new("column_names", LogicalType::list(LogicalType::Varchar)),
149        Field::new("column_types", LogicalType::list(LogicalType::Varchar)),
150        Field::new("temporary", LogicalType::Boolean),
151    ]
152}
153
154/// The columns `duckdb_columns()` returns, in the pin's order.
155#[must_use]
156pub fn column_fields() -> Vec<Field> {
157    vec![
158        Field::new("database_name", LogicalType::Varchar),
159        Field::new("database_oid", LogicalType::BigInt),
160        Field::new("schema_name", LogicalType::Varchar),
161        Field::new("schema_oid", LogicalType::BigInt),
162        Field::new("table_name", LogicalType::Varchar),
163        Field::new("table_oid", LogicalType::BigInt),
164        Field::new("column_name", LogicalType::Varchar),
165        Field::new("column_index", LogicalType::Integer),
166        Field::new("comment", LogicalType::Varchar),
167        Field::new("internal", LogicalType::Boolean),
168        Field::new("column_default", LogicalType::Varchar),
169        Field::new("is_nullable", LogicalType::Boolean),
170        Field::new("data_type", LogicalType::Varchar),
171        Field::new("data_type_id", LogicalType::BigInt),
172        Field::new("character_maximum_length", LogicalType::Integer),
173        Field::new("numeric_precision", LogicalType::Integer),
174        Field::new("numeric_precision_radix", LogicalType::Integer),
175        Field::new("numeric_scale", LogicalType::Integer),
176        Field::new("tags", tags()),
177        Field::new("is_generated", LogicalType::Boolean),
178        Field::new("generation_expression", LogicalType::Varchar),
179    ]
180}
181
182/// The canonical name of a type, which is what [`crate::typecatalog::type_oid`] is keyed by.
183///
184/// The type written out, minus whatever modifiers it carries. `DECIMAL(9,2)` and `DECIMAL(38,10)`
185/// are both the same type as far as `data_type_id` is concerned, and so are a list of integers and a
186/// list of strings, because the oid is `LogicalTypeId` and that enumeration has one entry for the
187/// type constructor rather than one per instance of it.
188#[must_use]
189pub fn canonical(ty: &LogicalType) -> String {
190    match ty {
191        LogicalType::Decimal { .. } => "DECIMAL".to_string(),
192        LogicalType::List(_) | LogicalType::Array(_, _) => "LIST".to_string(),
193        LogicalType::Map(_, _) => "MAP".to_string(),
194        LogicalType::Struct(_) => "STRUCT".to_string(),
195        LogicalType::Union(_) => "UNION".to_string(),
196        other => other.to_string(),
197    }
198}
199
200/// The three numeric columns of `duckdb_columns()`, which most types report nothing in.
201///
202/// Measured off the pin rather than reasoned about, because the answers are not what the column
203/// names suggest. `numeric_precision` on an integer is a count of bits and not of digits, so an
204/// `INTEGER` reports 32 with a radix of 2, and a `FLOAT` reports 24 and a `DOUBLE` 53 because those
205/// are the mantissa widths. A `DECIMAL` is the one type where the number means digits, so it reports
206/// its width with a radix of 10 and its scale. Every unsigned integer reports nothing at all, which
207/// looks like an oversight upstream and is reproduced here because a client reading these is reading
208/// them from DuckDB's side of the comparison.
209#[must_use]
210pub fn numeric_facts(ty: &LogicalType) -> (Option<i32>, Option<i32>, Option<i32>) {
211    let binary = |bits| (Some(bits), Some(2), Some(0));
212    match ty {
213        LogicalType::TinyInt => binary(8),
214        LogicalType::SmallInt => binary(16),
215        LogicalType::Integer => binary(32),
216        LogicalType::BigInt => binary(64),
217        LogicalType::HugeInt => binary(128),
218        LogicalType::Float => binary(24),
219        LogicalType::Double => binary(53),
220        LogicalType::Decimal { width, scale } => {
221            (Some(i32::from(*width)), Some(10), Some(i32::from(*scale)))
222        }
223        _ => (None, None, None),
224    }
225}
226
227/// The `MAP(VARCHAR, VARCHAR)` that every one of these tables carries at least one of.
228fn tags() -> LogicalType {
229    LogicalType::map(LogicalType::Varchar, LogicalType::Varchar)
230}
231
232#[cfg(test)]
233mod tests {
234    use rudb_common::LogicalType;
235
236    use super::{
237        canonical, column_fields, database_fields, numeric_facts, schema_fields, table_fields,
238        view_fields,
239    };
240
241    #[test]
242    fn the_five_tables_are_the_shape_the_pin_returns() {
243        assert_eq!(database_fields().len(), 11);
244        assert_eq!(schema_fields().len(), 10);
245        assert_eq!(table_fields().len(), 16);
246        assert_eq!(view_fields().len(), 13);
247        assert_eq!(column_fields().len(), 21);
248    }
249
250    /// The four values read off the pin, which are not the ones the column names suggest.
251    #[test]
252    fn a_numeric_precision_is_bits_everywhere_except_on_a_decimal() {
253        assert_eq!(numeric_facts(&LogicalType::Integer), (Some(32), Some(2), Some(0)));
254        assert_eq!(numeric_facts(&LogicalType::Double), (Some(53), Some(2), Some(0)));
255        assert_eq!(
256            numeric_facts(&LogicalType::Decimal { width: 9, scale: 2 }),
257            (Some(9), Some(10), Some(2))
258        );
259        // An unsigned integer reports nothing, which is upstream's answer and not an omission here.
260        assert_eq!(numeric_facts(&LogicalType::UBigInt), (None, None, None));
261        assert_eq!(numeric_facts(&LogicalType::Varchar), (None, None, None));
262    }
263
264    #[test]
265    fn a_types_modifiers_are_not_part_of_the_name_the_oid_is_keyed_by() {
266        assert_eq!(canonical(&LogicalType::Decimal { width: 9, scale: 2 }), "DECIMAL");
267        assert_eq!(canonical(&LogicalType::list(LogicalType::Integer)), "LIST");
268        assert_eq!(canonical(&LogicalType::Integer), "INTEGER");
269        assert_eq!(canonical(&LogicalType::TimestampTz), "TIMESTAMP WITH TIME ZONE");
270    }
271
272    /// The one column name that does not follow the rule the other four tables follow.
273    #[test]
274    fn a_schemas_own_oid_is_spelled_oid_and_not_schema_oid() {
275        assert_eq!(schema_fields()[0].name, "oid");
276        assert!(schema_fields().iter().all(|field| field.name != "schema_oid"));
277    }
278}