Skip to main content

rudb_catalog/
catalog.rs

1//! The catalog: attached databases, their schemas, and the tables in them.
2
3use std::fmt;
4
5use rudb_common::{Error, Field, Result};
6
7use crate::name::{QualifiedName, same_name};
8use crate::table::Table;
9use crate::view::View;
10
11/// The default attached database, which is the one an in-memory session gets.
12pub const DEFAULT_CATALOG: &str = "memory";
13/// The default schema inside it.
14pub const DEFAULT_SCHEMA: &str = "main";
15
16/// One attached database.
17#[derive(Debug, Clone)]
18pub struct Database {
19    name: String,
20    schemas: Vec<Schema>,
21}
22
23impl Database {
24    /// The name it is attached as.
25    #[must_use]
26    pub fn name(&self) -> &str {
27        &self.name
28    }
29
30    /// The schemas in it.
31    #[must_use]
32    pub fn schemas(&self) -> &[Schema] {
33        &self.schemas
34    }
35}
36
37/// What a name in a schema turned out to be.
38///
39/// Tables and views share one namespace, so a lookup that only asked about tables would answer that
40/// `v` does not exist when what is true is that `v` is a view. Every message that tells those two
41/// apart is spelled with this, and the spelling is the binary's: `Table` and `View`, capitalised,
42/// in sentences such as `Existing object "v" is of type View, trying to drop type Table`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Entry {
45    /// A table, which holds rows.
46    Table,
47    /// A view, which holds a query.
48    View,
49}
50
51impl fmt::Display for Entry {
52    fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
53        out.write_str(match self {
54            Self::Table => "Table",
55            Self::View => "View",
56        })
57    }
58}
59
60/// One schema.
61#[derive(Debug, Clone)]
62pub struct Schema {
63    name: String,
64    tables: Vec<Table>,
65    views: Vec<View>,
66}
67
68impl Schema {
69    /// A schema of that name with nothing in it.
70    fn empty(name: &str) -> Self {
71        Self { name: name.to_string(), tables: Vec::new(), views: Vec::new() }
72    }
73
74    /// The schema name.
75    #[must_use]
76    pub fn name(&self) -> &str {
77        &self.name
78    }
79
80    /// The tables in it.
81    #[must_use]
82    pub fn tables(&self) -> &[Table] {
83        &self.tables
84    }
85
86    /// The views in it.
87    #[must_use]
88    pub fn views(&self) -> &[View] {
89        &self.views
90    }
91
92    /// What a name in this schema is, if it is anything.
93    fn kind(&self, name: &str) -> Option<Entry> {
94        if self.tables.iter().any(|held| same_name(&held.name().table, name)) {
95            return Some(Entry::Table);
96        }
97        if self.views.iter().any(|held| same_name(&held.name().table, name)) {
98            return Some(Entry::View);
99        }
100        None
101    }
102}
103
104/// Every attached database, and the rule for turning a written name into one object.
105///
106/// # Errors it produces
107///
108/// The messages are DuckDB's, per `spec/12-duckdb-compat.md` section 12.5, because a great many
109/// tests in the wild assert on the text. What is missing is the `Did you mean "hits"?` line that
110/// upstream appends to a missing entry, which needs a similarity search over the catalog and a
111/// tie break rule that matches theirs. That is compatibility work rather than catalog work and it
112/// is not done here.
113#[derive(Debug, Clone)]
114pub struct Catalog {
115    databases: Vec<Database>,
116    default_catalog: String,
117    default_schema: String,
118}
119
120impl Default for Catalog {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl Catalog {
127    /// A catalog with `memory.main` in it and nothing else, which is what an in-memory session
128    /// starts from.
129    #[must_use]
130    pub fn new() -> Self {
131        Self {
132            databases: vec![Database {
133                name: DEFAULT_CATALOG.to_string(),
134                schemas: vec![Schema::empty(DEFAULT_SCHEMA)],
135            }],
136            default_catalog: DEFAULT_CATALOG.to_string(),
137            default_schema: DEFAULT_SCHEMA.to_string(),
138        }
139    }
140
141    /// The catalog an unqualified name resolves in.
142    #[must_use]
143    pub fn default_catalog(&self) -> &str {
144        &self.default_catalog
145    }
146
147    /// The schema an unqualified name resolves in.
148    #[must_use]
149    pub fn default_schema(&self) -> &str {
150        &self.default_schema
151    }
152
153    /// The attached databases.
154    #[must_use]
155    pub fn databases(&self) -> &[Database] {
156        &self.databases
157    }
158
159    /// Attaches an empty database with a `main` schema in it.
160    ///
161    /// # Errors
162    ///
163    /// If a database of that name is already attached.
164    pub fn attach(&mut self, name: &str) -> Result<()> {
165        if self.databases.iter().any(|held| same_name(&held.name, name)) {
166            return Err(Error::catalog(format!("Database with name \"{name}\" already exists!")));
167        }
168        self.databases.push(Database {
169            name: name.to_string(),
170            schemas: vec![Schema::empty(DEFAULT_SCHEMA)],
171        });
172        Ok(())
173    }
174
175    /// Creates a schema in an attached database.
176    ///
177    /// # Errors
178    ///
179    /// If the database is not attached, or a schema of that name is already in it.
180    pub fn create_schema(&mut self, catalog: &str, name: &str) -> Result<()> {
181        let database = self.database_mut(catalog)?;
182        if database.schemas.iter().any(|held| same_name(&held.name, name)) {
183            return Err(Error::catalog(format!("Schema with name \"{name}\" already exists!")));
184        }
185        database.schemas.push(Schema::empty(name));
186        Ok(())
187    }
188
189    /// Creates an empty table.
190    ///
191    /// # Errors
192    ///
193    /// If the database or the schema is missing, if a table or a view of that name is already
194    /// there, or if two columns have the same name.
195    pub fn create_table(&mut self, name: QualifiedName, columns: Vec<Field>) -> Result<()> {
196        let table = Table::new(name.clone(), columns)?;
197        let schema = self.schema_mut(&name.catalog, &name.schema)?;
198        if let Some(found) = schema.kind(&name.table) {
199            return Err(taken(found, &name.table));
200        }
201        schema.tables.push(table);
202        Ok(())
203    }
204
205    /// Creates a view.
206    ///
207    /// The body is not checked here. Whether it binds is the binder's question and it is asked
208    /// before this is called, because a view that cannot bind is refused at creation.
209    ///
210    /// # Errors
211    ///
212    /// If the database or the schema is missing, or if a table or a view of that name is already
213    /// there.
214    pub fn create_view(&mut self, view: View) -> Result<()> {
215        let name = view.name().clone();
216        let schema = self.schema_mut(&name.catalog, &name.schema)?;
217        if let Some(found) = schema.kind(&name.table) {
218            return Err(taken(found, &name.table));
219        }
220        schema.views.push(view);
221        Ok(())
222    }
223
224    /// Removes a table and everything in it.
225    ///
226    /// # Errors
227    ///
228    /// If there is no such table, or if the name is a view, which is a different sentence because
229    /// it is a different mistake.
230    pub fn drop_table(&mut self, name: &QualifiedName) -> Result<()> {
231        self.drop_entry(name, Entry::Table)
232    }
233
234    /// Removes a view.
235    ///
236    /// # Errors
237    ///
238    /// If there is no such view, or if the name is a table.
239    pub fn drop_view(&mut self, name: &QualifiedName) -> Result<()> {
240        self.drop_entry(name, Entry::View)
241    }
242
243    /// Removes whichever of the two the caller said it was dropping, refusing the other one.
244    fn drop_entry(&mut self, name: &QualifiedName, wanted: Entry) -> Result<()> {
245        let schema = self.schema_mut(&name.catalog, &name.schema)?;
246        match schema.kind(&name.table) {
247            // The type in this one is the type being dropped, so `DROP VIEW gone` is a missing view
248            // and `DROP TABLE gone` is a missing table over the same absent name.
249            None => Err(missing(wanted, &name.table)),
250            Some(found) if found != wanted => Err(Error::catalog(format!(
251                "Existing object \"{}\" is of type {found}, trying to drop type {wanted}",
252                name.table
253            ))),
254            Some(Entry::Table) => {
255                schema.tables.retain(|held| !same_name(&held.name().table, &name.table));
256                Ok(())
257            }
258            Some(Entry::View) => {
259                schema.views.retain(|held| !same_name(&held.name().table, &name.table));
260                Ok(())
261            }
262        }
263    }
264
265    /// What a full name is, if it is anything.
266    ///
267    /// # Errors
268    ///
269    /// If the database, the schema, or the name itself is missing.
270    pub fn entry(&self, name: &QualifiedName) -> Result<Entry> {
271        self.schema(&name.catalog, &name.schema)?
272            .kind(&name.table)
273            .ok_or_else(|| missing_table(&name.table))
274    }
275
276    /// A view by its full name.
277    ///
278    /// # Errors
279    ///
280    /// If the database, the schema or the view is missing.
281    pub fn view(&self, name: &QualifiedName) -> Result<&View> {
282        let schema = self.schema(&name.catalog, &name.schema)?;
283        schema
284            .views
285            .iter()
286            .find(|held| same_name(&held.name().table, &name.table))
287            .ok_or_else(|| missing_table(&name.table))
288    }
289
290    /// A table by its full name.
291    ///
292    /// # Errors
293    ///
294    /// If the database, the schema or the table is missing.
295    pub fn table(&self, name: &QualifiedName) -> Result<&Table> {
296        let schema = self.schema(&name.catalog, &name.schema)?;
297        schema
298            .tables
299            .iter()
300            .find(|held| same_name(&held.name().table, &name.table))
301            .ok_or_else(|| missing_table(&name.table))
302    }
303
304    /// A table by its full name, to change.
305    ///
306    /// # Errors
307    ///
308    /// If the database, the schema or the table is missing.
309    pub fn table_mut(&mut self, name: &QualifiedName) -> Result<&mut Table> {
310        let table = name.table.clone();
311        let schema = self.schema_mut(&name.catalog, &name.schema)?;
312        schema
313            .tables
314            .iter_mut()
315            .find(|held| same_name(&held.name().table, &table))
316            .ok_or_else(|| missing_table(&table))
317    }
318
319    /// Turns the parts of a written name into the full name of a table or a view that exists.
320    ///
321    /// One part is a table in the default schema. Three parts are a catalog, a schema and a table.
322    /// Two parts are the interesting case: they are a schema and a table if the first part names a
323    /// schema in the default catalog, and a catalog and a table otherwise, which is the order
324    /// DuckDB tries them in and matters for `information_schema.tables` and for `memory.hits`
325    /// meaning what they each look like they mean.
326    ///
327    /// The name that comes back is the one the object was created with rather than the one that was
328    /// written, so a plan built from it prints the spelling a person would recognise.
329    ///
330    /// # Errors
331    ///
332    /// If the name has no parts or more than three, or if it does not resolve to either.
333    pub fn resolve(&self, parts: &[&str]) -> Result<QualifiedName> {
334        self.resolve_as(parts, Entry::Table)
335    }
336
337    /// The same as [`Catalog::resolve`], except that a name which is not there is reported as a
338    /// missing `wanted` rather than as a missing table.
339    ///
340    /// A statement that says which of the two it meant gets to say it in the complaint, so `DROP
341    /// VIEW gone` is a missing view and `DROP TABLE gone` is a missing table over the same absent
342    /// name. A statement that does not say, such as a read, is resolving a table as far as the
343    /// message is concerned, which is why plain `resolve` passes [`Entry::Table`].
344    ///
345    /// # Errors
346    ///
347    /// If the name has no parts or more than three, or if it does not resolve to either.
348    pub fn resolve_as(&self, parts: &[&str], wanted: Entry) -> Result<QualifiedName> {
349        let candidates = self.candidates(parts)?;
350        let mut first_error = None;
351        for candidate in &candidates {
352            let held = match self.schema(&candidate.catalog, &candidate.schema) {
353                Ok(schema) => schema.kind(&candidate.table),
354                // The first reading is the preferred one, so its complaint is the one that names
355                // the piece the writer most likely meant and got wrong.
356                Err(error) => {
357                    first_error = first_error.or(Some(error));
358                    continue;
359                }
360            };
361            match held {
362                Some(Entry::Table) => return Ok(self.table(candidate)?.name().clone()),
363                Some(Entry::View) => return Ok(self.view(candidate)?.name().clone()),
364                None => {
365                    first_error = first_error.or_else(|| Some(missing(wanted, &candidate.table)));
366                }
367            }
368        }
369        Err(first_error.unwrap_or_else(|| missing(wanted, &parts.join("."))))
370    }
371
372    /// The full name a `CREATE` of this written name would make, without requiring it to exist.
373    ///
374    /// # Errors
375    ///
376    /// If the name has no parts or more than three, or if the schema it names is missing.
377    pub fn resolve_for_create(&self, parts: &[&str]) -> Result<QualifiedName> {
378        let candidates = self.candidates(parts)?;
379        let mut first_error = None;
380        for candidate in &candidates {
381            match self.schema(&candidate.catalog, &candidate.schema) {
382                Ok(_) => return Ok(candidate.clone()),
383                Err(error) => first_error = first_error.or(Some(error)),
384            }
385        }
386        Err(first_error.unwrap_or_else(|| {
387            Error::catalog(format!("Schema with name {} does not exist!", parts.join(".")))
388        }))
389    }
390
391    /// Every table, in creation order within a schema.
392    pub fn tables(&self) -> impl Iterator<Item = &Table> {
393        self.databases
394            .iter()
395            .flat_map(|database| database.schemas.iter())
396            .flat_map(|schema| schema.tables.iter())
397    }
398
399    /// The readings of a written name, best first.
400    fn candidates(&self, parts: &[&str]) -> Result<Vec<QualifiedName>> {
401        match parts {
402            [table] => {
403                Ok(vec![QualifiedName::new(&self.default_catalog, &self.default_schema, *table)])
404            }
405            [first, table] => Ok(vec![
406                QualifiedName::new(&self.default_catalog, *first, *table),
407                QualifiedName::new(*first, &self.default_schema, *table),
408            ]),
409            [catalog, schema, table] => Ok(vec![QualifiedName::new(*catalog, *schema, *table)]),
410            _ => Err(Error::catalog(format!(
411                "a name of {} parts, and a table name has one, two or three",
412                parts.len()
413            ))),
414        }
415    }
416
417    fn database(&self, catalog: &str) -> Result<&Database> {
418        self.databases
419            .iter()
420            .find(|held| same_name(&held.name, catalog))
421            .ok_or_else(|| Error::catalog(format!("Catalog with name {catalog} does not exist!")))
422    }
423
424    fn database_mut(&mut self, catalog: &str) -> Result<&mut Database> {
425        self.databases
426            .iter_mut()
427            .find(|held| same_name(&held.name, catalog))
428            .ok_or_else(|| Error::catalog(format!("Catalog with name {catalog} does not exist!")))
429    }
430
431    fn schema(&self, catalog: &str, schema: &str) -> Result<&Schema> {
432        self.database(catalog)?
433            .schemas
434            .iter()
435            .find(|held| same_name(&held.name, schema))
436            .ok_or_else(|| Error::catalog(format!("Schema with name {schema} does not exist!")))
437    }
438
439    fn schema_mut(&mut self, catalog: &str, schema: &str) -> Result<&mut Schema> {
440        self.database_mut(catalog)?
441            .schemas
442            .iter_mut()
443            .find(|held| same_name(&held.name, schema))
444            .ok_or_else(|| Error::catalog(format!("Schema with name {schema} does not exist!")))
445    }
446}
447
448fn missing_table(name: &str) -> Error {
449    Error::catalog(format!("Table with name {name} does not exist!"))
450}
451
452/// The error for a name that is not there, named after what was being looked for.
453///
454/// A read says table whatever the name turns out to be, because a query that reads from `v` is
455/// asking for a table and does not know or care that `v` could have been a view. A drop says which
456/// of the two it was dropping, because `DROP VIEW` said so.
457fn missing(wanted: Entry, name: &str) -> Error {
458    Error::catalog(format!("{wanted} with name {name} does not exist!"))
459}
460
461/// The error for creating something over a name that is already taken.
462///
463/// The type in the sentence is the one that is already there, not the one being created. `CREATE
464/// TABLE v` over an existing view `v` is `View with name "v" already exists!` and `CREATE VIEW t`
465/// over an existing table `t` is `Table with name "t" already exists!`, both measured against
466/// v2.0.0-dev84237 at cc7e7bac7f, which is the commit the grammar is vendored from.
467///
468/// It went the other way round in v1.5.1, where the sentence named the type being created. That
469/// reads backwards and upstream changed it, which is the argument for pinning the reference to the
470/// vendored commit rather than to whatever is released.
471fn taken(found: Entry, name: &str) -> Error {
472    Error::catalog(format!("{found} with name \"{name}\" already exists!"))
473}
474
475#[cfg(test)]
476mod tests {
477    use rudb_common::LogicalType;
478
479    use super::*;
480    use crate::view::View;
481
482    fn with_hits() -> Catalog {
483        let mut catalog = Catalog::new();
484        catalog
485            .create_table(
486                QualifiedName::new("memory", "main", "hits"),
487                vec![
488                    Field::new("UserID", LogicalType::BigInt),
489                    Field::new("SearchPhrase", LogicalType::Varchar),
490                ],
491            )
492            .expect("a table in the default schema");
493        catalog
494    }
495
496    #[test]
497    fn a_fresh_catalog_has_the_in_memory_database_in_it() {
498        let catalog = Catalog::new();
499        assert_eq!(catalog.default_catalog(), "memory");
500        assert_eq!(catalog.default_schema(), "main");
501        assert_eq!(catalog.databases().len(), 1);
502        assert_eq!(catalog.tables().count(), 0);
503    }
504
505    #[test]
506    fn an_unqualified_name_resolves_in_the_default_schema() {
507        let catalog = with_hits();
508        let name = catalog.resolve(&["HITS"]).expect("the table exists whatever the case");
509        assert_eq!(name.to_string(), "memory.main.hits");
510    }
511
512    #[test]
513    fn a_three_part_name_resolves_to_itself() {
514        let catalog = with_hits();
515        let name = catalog.resolve(&["memory", "main", "hits"]).expect("the full name");
516        assert_eq!(name.to_string(), "memory.main.hits");
517    }
518
519    /// Two parts are a schema and a table before they are a catalog and a table, which is what
520    /// makes `information_schema.tables` work, and they fall back to a catalog and a table, which
521    /// is what makes `memory.hits` work.
522    #[test]
523    fn two_parts_are_a_schema_first_and_a_catalog_second() {
524        let mut catalog = with_hits();
525        catalog.create_schema("memory", "reporting").expect("a second schema");
526        catalog
527            .create_table(
528                QualifiedName::new("memory", "reporting", "hits"),
529                vec![Field::new("n", LogicalType::Integer)],
530            )
531            .expect("a table in it");
532
533        let by_schema = catalog.resolve(&["reporting", "hits"]).expect("the reporting one");
534        assert_eq!(by_schema.to_string(), "memory.reporting.hits");
535
536        let by_catalog = catalog.resolve(&["memory", "hits"]).expect("the default schema one");
537        assert_eq!(by_catalog.to_string(), "memory.main.hits");
538    }
539
540    #[test]
541    fn a_table_that_is_not_there_says_so_the_way_duckdb_does() {
542        let catalog = with_hits();
543        let error = catalog.resolve(&["nope"]).expect_err("there is no table called nope");
544        assert_eq!(error.to_string(), "Catalog Error: Table with name nope does not exist!");
545    }
546
547    #[test]
548    fn a_schema_that_is_not_there_says_which_schema() {
549        let catalog = with_hits();
550        let error =
551            catalog.resolve(&["memory", "nope", "hits"]).expect_err("there is no schema nope");
552        assert_eq!(error.to_string(), "Catalog Error: Schema with name nope does not exist!");
553    }
554
555    #[test]
556    fn creating_the_same_table_twice_is_an_error() {
557        let mut catalog = with_hits();
558        let error = catalog
559            .create_table(
560                QualifiedName::new("memory", "main", "HITS"),
561                vec![Field::new("n", LogicalType::Integer)],
562            )
563            .expect_err("hits is already there");
564        assert_eq!(error.to_string(), "Catalog Error: Table with name \"HITS\" already exists!");
565    }
566
567    #[test]
568    fn a_dropped_table_is_gone() {
569        let mut catalog = with_hits();
570        let name = catalog.resolve(&["hits"]).expect("it is there");
571        catalog.drop_table(&name).expect("dropping it works");
572        assert!(catalog.resolve(&["hits"]).is_err(), "it is not there any more");
573        assert!(catalog.drop_table(&name).is_err(), "dropping it twice does not");
574    }
575
576    #[test]
577    fn a_name_for_a_create_does_not_have_to_exist_yet() {
578        let catalog = Catalog::new();
579        let name = catalog.resolve_for_create(&["new_table"]).expect("the default schema is there");
580        assert_eq!(name.to_string(), "memory.main.new_table");
581        assert!(
582            catalog.resolve_for_create(&["nope", "new_table"]).is_err(),
583            "a schema that is not there is still an error"
584        );
585    }
586
587    #[test]
588    fn a_name_of_four_parts_is_not_a_table_name() {
589        let catalog = Catalog::new();
590        let error = catalog.resolve(&["a", "b", "c", "d"]).expect_err("four parts");
591        assert!(error.message().contains("4 parts"), "{error}");
592    }
593
594    fn with_view() -> Catalog {
595        let mut catalog = with_hits();
596        catalog
597            .create_view(View::new(
598                QualifiedName::new("memory", "main", "recent"),
599                "SELECT * FROM hits".to_string(),
600                Vec::new(),
601            ))
602            .expect("a view in the default schema");
603        catalog
604    }
605
606    #[test]
607    fn a_view_resolves_the_way_a_table_does() {
608        let catalog = with_view();
609        let name = catalog.resolve(&["RECENT"]).expect("the view, whatever the case");
610        assert_eq!(name.to_string(), "memory.main.recent");
611        assert_eq!(catalog.entry(&name).expect("it is there"), Entry::View);
612        assert_eq!(catalog.view(&name).expect("the body").sql(), "SELECT * FROM hits");
613    }
614
615    #[test]
616    fn the_two_share_one_namespace_and_the_message_names_what_was_being_made() {
617        let mut catalog = with_view();
618        let error = catalog
619            .create_table(
620                QualifiedName::new("memory", "main", "recent"),
621                vec![Field::new("n", LogicalType::Integer)],
622            )
623            .expect_err("recent is a view");
624        assert_eq!(error.to_string(), "Catalog Error: View with name \"recent\" already exists!");
625
626        let error = catalog
627            .create_view(View::new(
628                QualifiedName::new("memory", "main", "HITS"),
629                "SELECT 1".to_string(),
630                Vec::new(),
631            ))
632            .expect_err("hits is a table");
633        assert_eq!(error.to_string(), "Catalog Error: Table with name \"HITS\" already exists!");
634    }
635
636    #[test]
637    fn dropping_one_as_the_other_names_both_types() {
638        let mut catalog = with_view();
639        let view = catalog.resolve(&["recent"]).expect("the view");
640        let error = catalog.drop_table(&view).expect_err("it is a view");
641        assert_eq!(
642            error.to_string(),
643            "Catalog Error: Existing object \"recent\" is of type View, trying to drop type Table"
644        );
645        let table = catalog.resolve(&["hits"]).expect("the table");
646        let error = catalog.drop_view(&table).expect_err("it is a table");
647        assert_eq!(
648            error.to_string(),
649            "Catalog Error: Existing object \"hits\" is of type Table, trying to drop type View"
650        );
651        catalog.drop_view(&view).expect("dropping it as what it is");
652        assert!(catalog.resolve(&["recent"]).is_err(), "it is gone");
653    }
654
655    #[test]
656    fn attaching_gives_a_second_database_with_its_own_main() {
657        let mut catalog = with_hits();
658        catalog.attach("other").expect("a second database");
659        catalog
660            .create_table(
661                QualifiedName::new("other", "main", "hits"),
662                vec![Field::new("n", LogicalType::Integer)],
663            )
664            .expect("a table of the same name in it");
665        assert_eq!(catalog.tables().count(), 2);
666        let name = catalog.resolve(&["other", "main", "hits"]).expect("the other one");
667        assert_eq!(name.catalog, "other");
668        assert!(catalog.attach("OTHER").is_err(), "attaching it twice does not work");
669    }
670}