Skip to main content

erdify_rs/
schema.rs

1use std::collections::HashSet;
2
3/// Represents a table with all its metadata.
4#[derive(Debug, Clone, Default)]
5pub struct Table {
6    pub schema: String,
7    pub name: String,
8    pub columns: Vec<Column>,
9    pub primary_keys: Vec<String>,
10    pub foreign_keys: Vec<ForeignKey>,
11    pub not_null_cols: HashSet<String>,
12    pub unique_constraints: Vec<UniqueConstraint>,
13    pub check_constraints: Vec<CheckConstraint>,
14    pub indexes: Vec<IndexInfo>,
15}
16
17impl Table {
18    /// Unique identifying key of a table: `(schema, name)`.
19    #[must_use]
20    pub fn key(&self) -> (&str, &str) {
21        (&self.schema, &self.name)
22    }
23}
24
25/// Column of a table.
26#[derive(Debug, Clone)]
27pub struct Column {
28    pub name: String,
29    pub data_type: String,
30}
31
32/// Foreign key constraint (potentially multi-column).
33#[derive(Debug, Clone)]
34pub struct ForeignKey {
35    pub name: String,
36    pub from_columns: Vec<String>,
37    pub to_schema: String,
38    pub to_table: String,
39    pub to_columns: Vec<String>,
40}
41
42/// UNIQUE constraint.
43#[derive(Debug, Clone)]
44pub struct UniqueConstraint {
45    pub name: String,
46    pub columns: Vec<String>,
47}
48
49/// CHECK constraint.
50#[derive(Debug, Clone)]
51pub struct CheckConstraint {
52    pub name: String,
53    pub definition: String,
54}
55
56/// Information about an index.
57#[derive(Debug, Clone)]
58pub struct IndexInfo {
59    pub name: String,
60    pub columns: Vec<String>,
61    pub is_unique: bool,
62}
63
64/// System schemas always excluded from generation.
65pub const SYSTEM_SCHEMAS: [&str; 2] = ["information_schema", "pg_catalog"];
66
67/// Filters tables based on the specified schemas and tables.
68///
69/// An empty filter list means "no filtering" on the corresponding criterion;
70/// system schemas remain excluded in all cases. `ignore_tables` removes the
71/// named tables from the result, regardless of their schema; `tables_filter`
72/// and `ignore_tables` are mutually exclusive on the CLI side (clap
73/// `conflicts_with`), so at most one of the two is non-empty here.
74#[must_use]
75pub fn filter_tables(
76    tables: Vec<Table>,
77    schemas: &[&str],
78    tables_filter: &[&str],
79    ignore_tables: &[&str],
80) -> Vec<Table> {
81    tables
82        .into_iter()
83        .filter(|t| {
84            let schema_ok = if schemas.is_empty() {
85                !SYSTEM_SCHEMAS.contains(&t.schema.as_str())
86            } else {
87                schemas.contains(&t.schema.as_str())
88            };
89
90            let table_ok = tables_filter.is_empty() || tables_filter.contains(&t.name.as_str());
91            let not_ignored = !ignore_tables.contains(&t.name.as_str());
92
93            schema_ok && table_ok && not_ignored
94        })
95        .collect()
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn table(schema: &str, name: &str) -> Table {
103        Table {
104            schema: schema.to_string(),
105            name: name.to_string(),
106            ..Table::default()
107        }
108    }
109
110    #[test]
111    fn filter_tables_excludes_system_schemas_by_default() {
112        let tables = vec![
113            table("public", "users"),
114            table("pg_catalog", "pg_class"),
115            table("information_schema", "columns"),
116        ];
117
118        let result = filter_tables(tables, &[], &[], &[]);
119
120        assert_eq!(result.len(), 1);
121        assert_eq!(result[0].name, "users");
122    }
123
124    #[test]
125    fn filter_tables_keeps_only_requested_schemas() {
126        let tables = vec![table("public", "users"), table("extended", "audit")];
127
128        let result = filter_tables(tables, &["extended"], &[], &[]);
129
130        assert_eq!(result.len(), 1);
131        assert_eq!(result[0].schema, "extended");
132    }
133
134    #[test]
135    fn filter_tables_combines_schema_and_table_filters() {
136        let tables = vec![
137            table("public", "users"),
138            table("public", "orders"),
139            table("extended", "users"),
140        ];
141
142        let result = filter_tables(tables, &["public"], &["users"], &[]);
143
144        assert_eq!(result.len(), 1);
145        assert_eq!(result[0].key(), ("public", "users"));
146    }
147
148    #[test]
149    fn filter_tables_excludes_ignored_tables() {
150        let tables = vec![
151            table("public", "users"),
152            table("public", "logs"),
153            table("extended", "logs"),
154        ];
155
156        let result = filter_tables(tables, &[], &[], &["logs"]);
157
158        assert_eq!(result.len(), 1);
159        assert_eq!(result[0].name, "users");
160    }
161
162    #[test]
163    fn filter_tables_combines_schema_and_ignore_filters() {
164        let tables = vec![
165            table("public", "users"),
166            table("public", "logs"),
167            table("extended", "logs"),
168        ];
169
170        let result = filter_tables(tables, &["public"], &[], &["logs"]);
171
172        assert_eq!(result.len(), 1);
173        assert_eq!(result[0].key(), ("public", "users"));
174    }
175}