Skip to main content

erdify_rs/
schema.rs

1use std::collections::HashSet;
2
3/// Kind of catalog relation an entity was extracted from.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum TableKind {
6    /// Ordinary or partitioned table.
7    #[default]
8    Table,
9    /// Plain (non-materialized) view.
10    View,
11    /// Materialized view.
12    MaterializedView,
13}
14
15/// Represents a table with all its metadata.
16#[derive(Debug, Clone, Default)]
17pub struct Table {
18    pub schema: String,
19    pub name: String,
20    pub kind: TableKind,
21    pub columns: Vec<Column>,
22    pub primary_keys: Vec<String>,
23    pub foreign_keys: Vec<ForeignKey>,
24    pub not_null_cols: HashSet<String>,
25    pub unique_constraints: Vec<UniqueConstraint>,
26    pub check_constraints: Vec<CheckConstraint>,
27    pub indexes: Vec<IndexInfo>,
28}
29
30impl Table {
31    /// Unique identifying key of a table: `(schema, name)`.
32    #[must_use]
33    pub fn key(&self) -> (&str, &str) {
34        (&self.schema, &self.name)
35    }
36}
37
38/// Column of a table.
39#[derive(Debug, Clone)]
40pub struct Column {
41    pub name: String,
42    pub data_type: String,
43    /// Raw `DEFAULT` expression, if any. Never set for views or materialized
44    /// views: PostgreSQL doesn't allow column defaults on them.
45    pub default: Option<String>,
46}
47
48/// Foreign key constraint (potentially multi-column).
49#[derive(Debug, Clone)]
50pub struct ForeignKey {
51    pub name: String,
52    pub from_columns: Vec<String>,
53    pub to_schema: String,
54    pub to_table: String,
55    pub to_columns: Vec<String>,
56}
57
58/// UNIQUE constraint.
59#[derive(Debug, Clone)]
60pub struct UniqueConstraint {
61    pub name: String,
62    pub columns: Vec<String>,
63}
64
65/// CHECK constraint.
66#[derive(Debug, Clone)]
67pub struct CheckConstraint {
68    pub name: String,
69    pub definition: String,
70}
71
72/// Information about an index.
73#[derive(Debug, Clone)]
74pub struct IndexInfo {
75    pub name: String,
76    pub columns: Vec<String>,
77    pub is_unique: bool,
78}
79
80/// System schemas always excluded from generation.
81pub const SYSTEM_SCHEMAS: [&str; 2] = ["information_schema", "pg_catalog"];
82
83/// Filters tables based on the specified schemas and tables.
84///
85/// An empty filter list means "no filtering" on the corresponding criterion;
86/// system schemas remain excluded in all cases. `ignore_tables` removes the
87/// named tables from the result, regardless of their schema; `tables_filter`
88/// and `ignore_tables` are mutually exclusive on the CLI side (clap
89/// `conflicts_with`), so at most one of the two is non-empty here.
90#[must_use]
91pub fn filter_tables(
92    tables: Vec<Table>,
93    schemas: &[&str],
94    tables_filter: &[&str],
95    ignore_tables: &[&str],
96) -> Vec<Table> {
97    tables
98        .into_iter()
99        .filter(|t| {
100            let schema_ok = if schemas.is_empty() {
101                !SYSTEM_SCHEMAS.contains(&t.schema.as_str())
102            } else {
103                schemas.contains(&t.schema.as_str())
104            };
105
106            let table_ok = tables_filter.is_empty() || tables_filter.contains(&t.name.as_str());
107            let not_ignored = !ignore_tables.contains(&t.name.as_str());
108
109            schema_ok && table_ok && not_ignored
110        })
111        .collect()
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn table(schema: &str, name: &str) -> Table {
119        Table {
120            schema: schema.to_string(),
121            name: name.to_string(),
122            ..Table::default()
123        }
124    }
125
126    fn view(schema: &str, name: &str, kind: TableKind) -> Table {
127        Table {
128            schema: schema.to_string(),
129            name: name.to_string(),
130            kind,
131            ..Table::default()
132        }
133    }
134
135    #[test]
136    fn filter_tables_excludes_system_schemas_by_default() {
137        let tables = vec![
138            table("public", "users"),
139            table("pg_catalog", "pg_class"),
140            table("information_schema", "columns"),
141        ];
142
143        let result = filter_tables(tables, &[], &[], &[]);
144
145        assert_eq!(result.len(), 1);
146        assert_eq!(result[0].name, "users");
147    }
148
149    #[test]
150    fn filter_tables_keeps_only_requested_schemas() {
151        let tables = vec![table("public", "users"), table("extended", "audit")];
152
153        let result = filter_tables(tables, &["extended"], &[], &[]);
154
155        assert_eq!(result.len(), 1);
156        assert_eq!(result[0].schema, "extended");
157    }
158
159    #[test]
160    fn filter_tables_combines_schema_and_table_filters() {
161        let tables = vec![
162            table("public", "users"),
163            table("public", "orders"),
164            table("extended", "users"),
165        ];
166
167        let result = filter_tables(tables, &["public"], &["users"], &[]);
168
169        assert_eq!(result.len(), 1);
170        assert_eq!(result[0].key(), ("public", "users"));
171    }
172
173    #[test]
174    fn filter_tables_excludes_ignored_tables() {
175        let tables = vec![
176            table("public", "users"),
177            table("public", "logs"),
178            table("extended", "logs"),
179        ];
180
181        let result = filter_tables(tables, &[], &[], &["logs"]);
182
183        assert_eq!(result.len(), 1);
184        assert_eq!(result[0].name, "users");
185    }
186
187    #[test]
188    fn filter_tables_treats_views_and_materialized_views_like_tables() {
189        let tables = vec![
190            table("public", "users"),
191            view("public", "active_users", TableKind::View),
192            view("public", "users_summary", TableKind::MaterializedView),
193            view("extended", "audit_view", TableKind::View),
194        ];
195
196        // --schema public --table active_users : le filtre par schéma et par
197        // nom s'applique aux vues exactement comme aux tables.
198        let result = filter_tables(tables.clone(), &["public"], &["active_users"], &[]);
199        assert_eq!(result.len(), 1);
200        assert_eq!(result[0].key(), ("public", "active_users"));
201        assert_eq!(result[0].kind, TableKind::View);
202
203        // --ignore-tables users_summary : exclut la vue matérialisée nommée,
204        // quel que soit son schéma.
205        let result = filter_tables(tables, &["public"], &[], &["users_summary"]);
206        let names: Vec<&str> = result.iter().map(|t| t.name.as_str()).collect();
207        assert_eq!(names, vec!["users", "active_users"]);
208    }
209
210    #[test]
211    fn filter_tables_combines_schema_and_ignore_filters() {
212        let tables = vec![
213            table("public", "users"),
214            table("public", "logs"),
215            table("extended", "logs"),
216        ];
217
218        let result = filter_tables(tables, &["public"], &[], &["logs"]);
219
220        assert_eq!(result.len(), 1);
221        assert_eq!(result[0].key(), ("public", "users"));
222    }
223}