Skip to main content

saya_types/
schema.rs

1use serde::{Deserialize, Serialize};
2
3/// A complete schema snapshot returned by a connector.
4#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
5pub struct SchemaTree {
6    pub databases: Vec<Database>,
7}
8
9impl SchemaTree {
10    /// Find a table by its three-part name, case-insensitive on each part —
11    /// the convention the schema fingerprint and the validity reconciler use,
12    /// so a name that kept its casing but changed type still resolves here.
13    /// Returns `None` when any part is absent. Pure lookup over the tree; the
14    /// caller decides what absence means (refuse, mark stale, fall back).
15    pub fn find_table(&self, catalog: &str, schema: &str, table: &str) -> Option<&Table> {
16        self.databases
17            .iter()
18            .find(|db| db.name.eq_ignore_ascii_case(catalog))
19            .and_then(|db| {
20                db.schemas
21                    .iter()
22                    .find(|s| s.name.eq_ignore_ascii_case(schema))
23            })
24            .and_then(|s| s.tables.iter().find(|t| t.name.eq_ignore_ascii_case(table)))
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct Database {
30    pub name: String,
31    #[serde(default)]
32    pub schemas: Vec<Schema>,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Schema {
37    pub name: String,
38    #[serde(default)]
39    pub tables: Vec<Table>,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct Table {
44    pub name: String,
45    #[serde(default)]
46    pub columns: Vec<Column>,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct Column {
51    pub name: String,
52    pub data_type: String,
53    pub nullable: bool,
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    fn tree_with(table: &str) -> SchemaTree {
61        SchemaTree {
62            databases: vec![Database {
63                name: "analytics".into(),
64                schemas: vec![Schema {
65                    name: "public".into(),
66                    tables: vec![Table {
67                        name: table.into(),
68                        columns: vec![Column {
69                            name: "id".into(),
70                            data_type: "bigint".into(),
71                            nullable: false,
72                        }],
73                    }],
74                }],
75            }],
76        }
77    }
78
79    #[test]
80    fn find_table_resolves_case_insensitively() {
81        let tree = tree_with("orders");
82        let found = tree
83            .find_table("Analytics", "PUBLIC", "Orders")
84            .expect("case-insensitive on every part");
85        assert_eq!(found.name, "orders");
86    }
87
88    #[test]
89    fn find_table_none_when_any_part_absent() {
90        let tree = tree_with("orders");
91        assert!(tree.find_table("analytics", "public", "ghost").is_none());
92        assert!(tree.find_table("analytics", "private", "orders").is_none());
93        assert!(tree.find_table("other", "public", "orders").is_none());
94    }
95
96    #[test]
97    fn find_table_none_on_empty_tree() {
98        let tree = SchemaTree::default();
99        assert!(tree.find_table("analytics", "public", "orders").is_none());
100    }
101}