sqlant/
sql_entities.rs

1use serde::Serialize;
2use std::collections::{BTreeMap, BTreeSet};
3use std::sync::Arc;
4use std::vec::Vec;
5
6#[derive(Debug, Clone, Serialize, PartialEq)]
7pub struct TableColumn {
8    pub name: String,
9    pub col_num: i16, // Can be used like id of column
10    // Different SQL databases have different types.
11    // Let's keep it as string not ENUM
12    pub datatype: String,
13    pub constraints: BTreeSet<ColumnConstraints>,
14}
15
16impl TableColumn {
17    pub fn is_pk(&self) -> bool {
18        self.constraints.contains(&ColumnConstraints::PrimaryKey)
19    }
20    pub fn is_fk(&self) -> bool {
21        self.constraints.contains(&ColumnConstraints::ForeignKey)
22    }
23    pub fn is_nn(&self) -> bool {
24        self.constraints.contains(&ColumnConstraints::NotNull)
25    }
26}
27
28#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
29pub enum ColumnConstraints {
30    NotNull,
31    PrimaryKey,
32    ForeignKey,
33    Unique,
34    Check(String), // TODO Ex: CONSTRAINT CHK_Person CHECK (Age>=18 AND City='Sandnes') // +
35    Default(String), // TODO
36    Index,         // Mark NOT Unique indexes
37}
38
39// Types of relationship https://launchschool.com/books/sql_first_edition/read/multi_tables
40#[derive(Clone, Debug)]
41pub struct ForeignKey {
42    pub source_table: Arc<Table>,
43    pub source_columns: Vec<Arc<TableColumn>>,
44    pub target_table: Arc<Table>,
45    pub target_columns: Vec<Arc<TableColumn>>,
46    pub is_zero_one_to_one: bool, // 0..1 to 1
47}
48
49#[derive(Debug, Clone)]
50pub struct Table {
51    pub name: String,
52    pub columns: Vec<Arc<TableColumn>>,
53    pub has_composite_pk: bool,
54}
55
56#[derive(Clone, Debug, PartialEq)]
57pub struct View {
58    pub materialized: bool,
59    pub name: String,
60    pub columns: Vec<Arc<TableColumn>>,
61}
62
63// key - enum_name (type) v = enum values
64pub type SqlEnums = BTreeMap<String, Vec<String>>;
65
66// ERD - entity relationship diagram
67#[derive(Clone, Debug)]
68pub struct SqlERData {
69    pub tables: Vec<Arc<Table>>,
70    pub foreign_keys: Vec<ForeignKey>,
71    pub enums: SqlEnums,
72    pub views: Vec<Arc<View>>,
73}
74
75#[async_trait::async_trait]
76pub trait SqlERDataLoader {
77    // Connection string has to be passed in "constructor"
78    async fn load_erd_data(&mut self) -> Result<SqlERData, crate::SqlantError>;
79}
80
81impl Table {
82    pub fn new(name: String, columns: Vec<Arc<TableColumn>>) -> Table {
83        let has_composite_pk = columns.iter().fold(0, |acc, x| {
84            acc + x // there is a bit overhead here, because we can interrupt after acc > 1
85                .as_ref() // but it looks nicely than `for ... in ...` loop
86                .constraints
87                .contains(&ColumnConstraints::PrimaryKey) as u16
88        }) > 1; // if more than 1 pk, it is composite
89
90        Table {
91            name,
92            columns,
93            has_composite_pk,
94        }
95    }
96}
97
98impl ForeignKey {
99    fn is_zero_one_to_one(
100        source_table: &Arc<Table>,
101        source_columns: &[Arc<TableColumn>],
102        target_table: &Arc<Table>,
103        target_columns: &[Arc<TableColumn>],
104    ) -> bool {
105        if source_columns.iter().any(|col| !col.is_pk()) {
106            return false;
107        }
108        if target_columns.iter().any(|col| !col.is_pk()) {
109            return false;
110        }
111
112        // It is not is_zero_one_to_one,
113        // If total count of source_table pks and target_table pks differs
114        if source_table.columns.iter().filter(|c| c.is_pk()).count()
115            != target_table.columns.iter().filter(|c| c.is_pk()).count()
116        {
117            return false;
118        }
119
120        true
121    }
122
123    pub fn new(
124        source_table: Arc<Table>,
125        source_columns: Vec<Arc<TableColumn>>,
126        target_table: Arc<Table>,
127        target_columns: Vec<Arc<TableColumn>>,
128    ) -> ForeignKey {
129        let is_zero_one_to_one = Self::is_zero_one_to_one(
130            &source_table,
131            &source_columns,
132            &target_table,
133            &target_columns,
134        );
135        ForeignKey {
136            source_table,
137            source_columns,
138            target_table,
139            target_columns,
140            is_zero_one_to_one,
141        }
142    }
143}