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