1
use crate::constants;
2
use canyon_crud::{crud::Transaction, DatabaseType, DatasourceConfig};
3
use regex::Regex;
4
use std::collections::HashMap;
5
use std::fs;
6
use walkdir::WalkDir;
7

            
8
use canyon_entities::register_types::CanyonRegisterEntity;
9

            
10
/// Convenient struct that contains the necessary data and operations to implement
11
/// the `Canyon Memory`.
12
///
13
/// Canyon Memory it's just a convenient way of relate the data of a Rust source
14
/// code file and the `CanyonEntity` (if so), helping Canyon to know what source
15
/// file contains a `#[canyon_entity]` annotation and restricting it to just one
16
/// annotated struct per file.
17
///
18
/// This limitation it's imposed by design. Canyon, when manages all the entities in
19
/// the user's source code, needs to know for future migrations the old data about a structure
20
/// and the new modified one.
21
///
22
/// For example, let's say that you have a:
23
/// ```
24
/// pub struct Person {
25
///    /* some fields */
26
/// }
27
/// ```
28
///
29
/// and you decided to modify it's Ident and change it to `Human`.
30
///
31
/// Canyon will take care about modifying the Database, and `ALTER TABLE` to edit the actual data for you,
32
/// but, if it's not able to get the data to know that the old one is `Person` and the new one it's `Human`.
33
/// it will simply drop the table (losing all your data) and creating a new table `Human`.
34
///
35
/// So, we decised to follow the next approach:
36
/// Every entity annotated with a `#[canyon_entity]` annotation will be related to only unique Rust source
37
/// code file. If we find more, Canyon will raise and error saying that it does not allows to having more than
38
/// one managed entity per source file.
39
///
40
/// Then, we will store the entities data in a special table only for Canyon, where we will create the relation
41
/// between the source file, the entity and it's fields and data.
42
///
43
/// So, if the user wants or needs to modify the data of it's entity, Canyon can secure that will perform the
44
/// correct operations because we can't "remember" how that entity was, and how it should be now, avoiding
45
/// potentially dangerous operations due to lack of knowing what entity relates with new data.
46
///
47
/// The `memory field` HashMap is made by the filepath as a key, and the struct's name as value
48
#[derive(Debug)]
49
pub struct CanyonMemory {
50
    pub memory: Vec<CanyonMemoryAnalyzer>,
51
    pub renamed_entities: HashMap<String, String>,
52
}
53

            
54
// Makes this structure able to make queries to the database
55
impl Transaction<Self> for CanyonMemory {}
56

            
57
impl CanyonMemory {
58
    /// Queries the database to retrieve internal data about the structures
59
    /// tracked by `CanyonSQL`
60
    #[cfg(not(cargo_check))]
61
    #[allow(clippy::nonminimal_bool)]
62
    pub async fn remember(
63
        datasource: &DatasourceConfig,
64
        canyon_entities: &[CanyonRegisterEntity<'_>],
65
    ) -> Self {
66
        // Creates the memory table if not exists
67
        Self::create_memory(&datasource.name, &datasource.get_db_type()).await;
68

            
69
        // Retrieve the last status data from the `canyon_memory` table
70
        let res = Self::query("SELECT * FROM canyon_memory", [], &datasource.name)
71
            .await
72
            .expect("Error querying Canyon Memory");
73

            
74
        // Manually maps the results
75
        let mut db_rows = Vec::new();
76
        #[cfg(feature = "postgres")]
77
        {
78
            let mem_results: &Vec<tokio_postgres::Row> = res.get_postgres_rows();
79
            for row in mem_results {
80
                let db_row = CanyonMemoryRow {
81
                    id: row.get::<&str, i32>("id"),
82
                    filepath: row.get::<&str, String>("filepath"),
83
                    struct_name: row.get::<&str, String>("struct_name").to_owned(),
84
                    declared_table_name: row.get::<&str, String>("declared_table_name").to_owned(),
85
                };
86
                db_rows.push(db_row);
87
            }
88
        }
89
        #[cfg(feature = "mssql")]
90
        {
91
            let mem_results: &Vec<tiberius::Row> = res.get_tiberius_rows();
92
            for row in mem_results {
93
                let db_row = CanyonMemoryRow {
94
                    id: row.get::<i32, &str>("id").unwrap(),
95
                    filepath: row.get::<&str, &str>("filepath").unwrap().to_string(),
96
                    struct_name: row.get::<&str, &str>("struct_name").unwrap().to_string(),
97
                    declared_table_name: row
98
                        .get::<&str, &str>("declared_table_name")
99
                        .unwrap()
100
                        .to_string(),
101
                };
102
                db_rows.push(db_row);
103
            }
104
        }
105

            
106
        Self::populate_memory(datasource, canyon_entities, db_rows).await
107
    }
108

            
109
    async fn populate_memory(
110
        datasource: &DatasourceConfig,
111
        canyon_entities: &[CanyonRegisterEntity<'_>],
112
        db_rows: Vec<CanyonMemoryRow>,
113
    ) -> CanyonMemory {
114
        let mut mem = Self {
115
            memory: Vec::new(),
116
            renamed_entities: HashMap::new(),
117
        };
118
        Self::find_canyon_entity_annotated_structs(&mut mem, canyon_entities).await;
119

            
120
        let mut updates = Vec::new();
121

            
122
        for _struct in &mem.memory {
123
            // For every program entity detected
124
            let already_in_db = db_rows.iter().find(|el| {
125
                el.filepath == _struct.filepath
126
                    || el.struct_name == _struct.struct_name
127
                    || el.declared_table_name == _struct.declared_table_name
128
            });
129

            
130
            if let Some(old) = already_in_db {
131
                if !(old.filepath == _struct.filepath
132
                    && old.struct_name == _struct.struct_name
133
                    && old.declared_table_name == _struct.declared_table_name)
134
                {
135
                    updates.push(&old.struct_name);
136
                    let stmt = format!(
137
                        "UPDATE canyon_memory SET filepath = '{}', struct_name = '{}', declared_table_name = '{}' \
138
                                WHERE id = {}",
139
                        _struct.filepath, _struct.struct_name, _struct.declared_table_name, old.id
140
                    );
141
                    save_canyon_memory_query(stmt, &datasource.name);
142

            
143
                    // if the updated element is the struct name, we add it to the table_rename Hashmap
144
                    let rename_table = old.declared_table_name != _struct.declared_table_name;
145

            
146
                    if rename_table {
147
                        mem.renamed_entities.insert(
148
                            _struct.declared_table_name.to_string(), // The new one
149
                            old.declared_table_name.to_string(),     // The old one
150
                        );
151
                    }
152
                }
153
            }
154

            
155
            if already_in_db.is_none() {
156
                let stmt = format!(
157
                    "INSERT INTO canyon_memory (filepath, struct_name, declared_table_name) \
158
                        VALUES ('{}', '{}', '{}')",
159
                    _struct.filepath, _struct.struct_name, _struct.declared_table_name
160
                );
161
                save_canyon_memory_query(stmt, &datasource.name)
162
            }
163
        }
164

            
165
        // Deletes the records from canyon_memory, because they stopped to be tracked by Canyon
166
        for db_row in db_rows.iter() {
167
            if !mem
168
                .memory
169
                .iter()
170
                .any(|entity| entity.struct_name == db_row.struct_name)
171
                && !updates.contains(&&(db_row.struct_name))
172
            {
173
                save_canyon_memory_query(
174
                    format!(
175
                        "DELETE FROM canyon_memory WHERE struct_name = '{}'",
176
                        db_row.struct_name
177
                    ),
178
                    &datasource.name,
179
                );
180
            }
181
        }
182
        mem
183
    }
184

            
185
    /// Parses the Rust source code files to find the one who contains Canyon entities
186
    /// ie -> annotated with `#[canyon_entity]`
187
    #[cfg(not(cargo_check))]
188
    async fn find_canyon_entity_annotated_structs(
189
        &mut self,
190
        canyon_entities: &[CanyonRegisterEntity<'_>],
191
    ) {
192
        for file in WalkDir::new("./src")
193
            .into_iter()
194
            .filter_map(|file| file.ok())
195
        {
196
            if file.metadata().unwrap().is_file()
197
                && file.path().display().to_string().ends_with(".rs")
198
            {
199
                // Opening the source code file
200
                let contents =
201
                    fs::read_to_string(file.path()).expect("Something went wrong reading the file");
202

            
203
                let mut canyon_entity_macro_counter = 0;
204
                let mut struct_name = String::new();
205
                for line in contents.split('\n') {
206
                    if line.contains("#[") // separated checks for possible different paths
207
                        && line.contains("canyon_entity")
208
                        && !line.starts_with("//")
209
                    {
210
                        canyon_entity_macro_counter += 1;
211
                    }
212

            
213
                    let re = Regex::new(r#"\bstruct\s+(\w+)"#).unwrap();
214
                    if let Some(captures) = re.captures(line) {
215
                        struct_name.push_str(captures.get(1).unwrap().as_str());
216
                    }
217
                }
218

            
219
                // This limitation will be removed in future versions, when the memory
220
                // will be able to track every aspect of an entity
221
                match canyon_entity_macro_counter {
222
                    0 => (),
223
                    1 => {
224
                        let canyon_entity = canyon_entities
225
                            .iter()
226
                            .find(|ce| ce.entity_name == struct_name);
227
                        if let Some(c_entity) = canyon_entity {
228
                            self.memory.push(CanyonMemoryAnalyzer {
229
                                filepath: file.path().display().to_string().replace('\\', "/"),
230
                                struct_name: struct_name.clone(),
231
                                declared_table_name: c_entity.entity_db_table_name.to_string(),
232
                            })
233
                        }
234
                    }
235
                    _ => panic!(
236
                        "Canyon-SQL does not support having multiple structs annotated
237
                        with `#[canyon::entity]` on the same file when the migrations are enabled"
238
                    ),
239
                }
240
            }
241
        }
242
    }
243

            
244
    /// Generates, if not exists the `canyon_memory` table
245
    async fn create_memory(datasource_name: &str, database_type: &DatabaseType) {
246
        let query = match database_type {
247
            #[cfg(feature = "postgres")]
248
            DatabaseType::PostgreSql => constants::postgresql_queries::CANYON_MEMORY_TABLE,
249
            #[cfg(feature = "mssql")]
250
            DatabaseType::SqlServer => constants::mssql_queries::CANYON_MEMORY_TABLE,
251
            #[cfg(feature = "mysql")]
252
            DatabaseType::MySQL => todo!("Memory table in mysql not implemented"),
253
        };
254

            
255
        Self::query(query, [], datasource_name)
256
            .await
257
            .expect("Error creating the 'canyon_memory' table");
258
    }
259
}
260

            
261
fn save_canyon_memory_query(stmt: String, ds_name: &str) {
262
    use crate::CM_QUERIES_TO_EXECUTE;
263

            
264
    if CM_QUERIES_TO_EXECUTE.lock().unwrap().contains_key(ds_name) {
265
        CM_QUERIES_TO_EXECUTE
266
            .lock()
267
            .unwrap()
268
            .get_mut(ds_name)
269
            .unwrap()
270
            .push(stmt);
271
    } else {
272
        CM_QUERIES_TO_EXECUTE
273
            .lock()
274
            .unwrap()
275
            .insert(ds_name.to_owned(), vec![stmt]);
276
    }
277
}
278

            
279
/// Represents a single row from the `canyon_memory` table
280
#[derive(Debug)]
281
struct CanyonMemoryRow {
282
    id: i32,
283
    filepath: String,
284
    struct_name: String,
285
    declared_table_name: String,
286
}
287

            
288
/// Represents the data that will be serialized in the `canyon_memory` table
289
#[derive(Debug)]
290
pub struct CanyonMemoryAnalyzer {
291
    pub filepath: String,
292
    pub struct_name: String,
293
    pub declared_table_name: String,
294
}