dmsc/database/orm/
mod.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # ORM Module
19//!
20//! This module provides ORM-like database operations for DMSC.
21
22use crate::core::{DMSCResult, DMSCError};
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use crate::database::DMSCDatabase;
26
27pub mod repository;
28
29pub use repository::{DMSCORMSimpleRepository, DMSCORMCrudRepository, DMSCORMRepository};
30
31#[cfg(feature = "pyo3")]
32pub mod py_repository;
33
34#[cfg(feature = "pyo3")]
35pub use py_repository::DMSCPyORMRepository;
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
39pub struct ColumnDefinition {
40    pub name: String,
41    pub column_type: String,
42    pub is_primary_key: bool,
43    pub is_nullable: bool,
44    pub default_value: Option<String>,
45    pub is_unique: bool,
46    pub max_length: Option<usize>,
47}
48
49impl Default for ColumnDefinition {
50    fn default() -> Self {
51        Self {
52            name: String::new(),
53            column_type: "TEXT".to_string(),
54            is_primary_key: false,
55            is_nullable: true,
56            default_value: None,
57            is_unique: false,
58            max_length: None,
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
65pub struct IndexDefinition {
66    pub name: String,
67    pub columns: Vec<String>,
68    pub is_unique: bool,
69    pub is_full_text: bool,
70}
71
72impl Default for IndexDefinition {
73    fn default() -> Self {
74        Self {
75            name: String::new(),
76            columns: Vec::new(),
77            is_unique: false,
78            is_full_text: false,
79        }
80    }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
85pub struct ForeignKeyDefinition {
86    pub name: String,
87    pub column: String,
88    pub referenced_table: String,
89    pub referenced_column: String,
90    pub on_delete: String,
91    pub on_update: String,
92}
93
94impl Default for ForeignKeyDefinition {
95    fn default() -> Self {
96        Self {
97            name: String::new(),
98            column: String::new(),
99            referenced_table: String::new(),
100            referenced_column: String::new(),
101            on_delete: "CASCADE".to_string(),
102            on_update: "CASCADE".to_string(),
103        }
104    }
105}
106
107#[derive(Debug, Clone)]
108#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
109pub struct TableDefinition {
110    pub table_name: String,
111    pub columns: HashMap<String, ColumnDefinition>,
112    pub primary_key: Vec<String>,
113    pub indexes: Vec<IndexDefinition>,
114    pub foreign_keys: Vec<ForeignKeyDefinition>,
115    pub engine: Option<String>,
116    pub charset: Option<String>,
117}
118
119impl TableDefinition {
120    pub fn new(table_name: &str) -> Self {
121        Self {
122            table_name: table_name.to_string(),
123            columns: HashMap::new(),
124            primary_key: Vec::new(),
125            indexes: Vec::new(),
126            foreign_keys: Vec::new(),
127            engine: None,
128            charset: None,
129        }
130    }
131
132    pub fn add_column(&mut self, column: ColumnDefinition) {
133        self.columns.insert(column.name.clone(), column);
134    }
135
136    pub fn set_primary_key(&mut self, columns: Vec<String>) {
137        self.primary_key = columns;
138    }
139
140    pub fn add_index(&mut self, index: IndexDefinition) {
141        self.indexes.push(index);
142    }
143
144    pub fn add_foreign_key(&mut self, fk: ForeignKeyDefinition) {
145        self.foreign_keys.push(fk);
146    }
147
148    pub fn get_create_sql(&self) -> String {
149        let mut sql = format!("CREATE TABLE IF NOT EXISTS {} (", self.table_name);
150
151        let mut column_defs = Vec::new();
152
153        for (name, col) in &self.columns {
154            let mut def = format!("{} {}", name, col.column_type);
155
156            if !col.is_nullable {
157                def.push_str(" NOT NULL");
158            }
159
160            if col.is_primary_key {
161                def.push_str(" PRIMARY KEY");
162            }
163
164            if let Some(default) = &col.default_value {
165                def.push_str(&format!(" DEFAULT {}", default));
166            }
167
168            if col.is_unique {
169                def.push_str(" UNIQUE");
170            }
171
172            column_defs.push(def);
173        }
174
175        if !self.primary_key.is_empty() {
176            column_defs.push(format!("PRIMARY KEY ({})", self.primary_key.join(", ")));
177        }
178
179        sql.push_str(&column_defs.join(", "));
180
181        for fk in &self.foreign_keys {
182            sql.push_str(&format!(
183                ", FOREIGN KEY ({}) REFERENCES {}({}) ON DELETE {} ON UPDATE {}",
184                fk.column, fk.referenced_table, fk.referenced_column, fk.on_delete, fk.on_update
185            ));
186        }
187
188        sql.push_str(")");
189
190        if let Some(engine) = &self.engine {
191            sql.push_str(&format!(" ENGINE={}", engine));
192        }
193
194        if let Some(charset) = &self.charset {
195            sql.push_str(&format!(" DEFAULT CHARSET={}", charset));
196        }
197
198        sql.push_str(";");
199
200        sql
201    }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
206pub enum ComparisonOperator {
207    Equal,
208    NotEqual,
209    GreaterThan,
210    GreaterThanOrEqual,
211    LessThan,
212    LessThanOrEqual,
213    Like,
214    ILike,
215    In,
216    NotIn,
217    IsNull,
218    IsNotNull,
219    Between,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
224pub enum LogicalOperator {
225    And,
226    Or,
227    Not,
228}
229
230#[derive(Debug, Clone)]
231#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
232pub struct Criteria {
233    pub column: String,
234    pub operator: ComparisonOperator,
235    pub value: serde_json::Value,
236}
237
238impl Criteria {
239    pub fn new(column: &str, operator: ComparisonOperator, value: serde_json::Value) -> Self {
240        Self {
241            column: column.to_string(),
242            operator,
243            value,
244        }
245    }
246
247    pub fn to_sql(&self) -> (String, Vec<serde_json::Value>) {
248        let (op_str, value) = match self.operator {
249            ComparisonOperator::Equal => ("=".to_string(), vec![self.value.clone()]),
250            ComparisonOperator::NotEqual => ("!=".to_string(), vec![self.value.clone()]),
251            ComparisonOperator::GreaterThan => (">".to_string(), vec![self.value.clone()]),
252            ComparisonOperator::GreaterThanOrEqual => (">=".to_string(), vec![self.value.clone()]),
253            ComparisonOperator::LessThan => ("<".to_string(), vec![self.value.clone()]),
254            ComparisonOperator::LessThanOrEqual => ("<=".to_string(), vec![self.value.clone()]),
255            ComparisonOperator::Like => ("LIKE".to_string(), vec![self.value.clone()]),
256            ComparisonOperator::ILike => ("ILIKE".to_string(), vec![self.value.clone()]),
257            ComparisonOperator::In => {
258                if let serde_json::Value::Array(arr) = &self.value {
259                    let placeholders = (0..arr.len()).map(|_| "?").collect::<Vec<_>>().join(", ");
260                    (format!("IN ({})", placeholders), arr.clone())
261                } else {
262                    ("= ?".to_string(), vec![self.value.clone()])
263                }
264            }
265            ComparisonOperator::NotIn => {
266                if let serde_json::Value::Array(arr) = &self.value {
267                    let placeholders = (0..arr.len()).map(|_| "?").collect::<Vec<_>>().join(", ");
268                    (format!("NOT IN ({})", placeholders), arr.clone())
269                } else {
270                    ("!= ?".to_string(), vec![self.value.clone()])
271                }
272            }
273            ComparisonOperator::IsNull => ("IS NULL".to_string(), Vec::new()),
274            ComparisonOperator::IsNotNull => ("IS NOT NULL".to_string(), Vec::new()),
275            ComparisonOperator::Between => {
276                if let serde_json::Value::Array(arr) = &self.value {
277                    if arr.len() == 2 {
278                        ("BETWEEN ? AND ?".to_string(), arr.clone())
279                    } else {
280                        ("BETWEEN ? AND ?".to_string(), vec![self.value.clone()])
281                    }
282                } else {
283                    ("BETWEEN ? AND ?".to_string(), vec![self.value.clone()])
284                }
285            }
286        };
287
288        let placeholder = if matches!(self.operator, ComparisonOperator::IsNull | ComparisonOperator::IsNotNull) {
289            "".to_string()
290        } else {
291            "?".to_string()
292        };
293
294        (format!("{} {} {}", self.column, op_str, placeholder), value)
295    }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
299#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
300pub struct SortOrder {
301    pub column: String,
302    pub ascending: bool,
303}
304
305impl SortOrder {
306    pub fn new(column: &str, ascending: bool) -> Self {
307        Self {
308            column: column.to_string(),
309            ascending,
310        }
311    }
312
313    pub fn asc(column: &str) -> Self {
314        Self::new(column, true)
315    }
316
317    pub fn desc(column: &str) -> Self {
318        Self::new(column, false)
319    }
320}
321
322#[derive(Debug, Clone)]
323#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
324pub struct Pagination {
325    pub page: u64,
326    pub page_size: u64,
327}
328
329impl Default for Pagination {
330    fn default() -> Self {
331        Self {
332            page: 1,
333            page_size: 20,
334        }
335    }
336}
337
338impl Pagination {
339    pub fn new(page: u64, page_size: u64) -> Self {
340        Self { page, page_size }
341    }
342
343    pub fn offset(&self) -> u64 {
344        (self.page - 1) * self.page_size
345    }
346
347    pub fn limit(&self) -> u64 {
348        self.page_size
349    }
350}
351
352#[derive(Debug, Clone)]
353#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
354pub struct QueryBuilder {
355    pub table_name: String,
356    pub criteria: Vec<Criteria>,
357    pub sort_orders: Vec<SortOrder>,
358    pub pagination: Option<Pagination>,
359    pub select_columns: Option<Vec<String>>,
360    pub group_by_columns: Option<Vec<String>>,
361    pub having_criteria: Vec<Criteria>,
362    pub distinct: bool,
363    pub joins: Vec<JoinClause>,
364}
365
366#[derive(Debug, Clone)]
367#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
368pub struct JoinClause {
369    pub join_type: JoinType,
370    pub table_name: String,
371    pub on_column: String,
372    pub referenced_column: String,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
376#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
377pub enum JoinType {
378    Inner,
379    Left,
380    Right,
381    Full,
382}
383
384impl QueryBuilder {
385    pub fn new(table_name: &str) -> Self {
386        Self {
387            table_name: table_name.to_string(),
388            criteria: Vec::new(),
389            sort_orders: Vec::new(),
390            pagination: None,
391            select_columns: None,
392            group_by_columns: None,
393            having_criteria: Vec::new(),
394            distinct: false,
395            joins: Vec::new(),
396        }
397    }
398
399    pub fn select(&mut self, columns: Vec<&str>) -> &mut Self {
400        self.select_columns = Some(columns.iter().map(|s| s.to_string()).collect());
401        self
402    }
403
404    pub fn where_criteria(&mut self, criteria: Criteria) -> &mut Self {
405        self.criteria.push(criteria);
406        self
407    }
408
409    pub fn and_where(&mut self, criteria: Criteria) -> &mut Self {
410        self.where_criteria(criteria)
411    }
412
413    pub fn or_where(&mut self, criteria: Criteria) -> &mut Self {
414        self.criteria.push(criteria);
415        self
416    }
417
418    pub fn order_by(&mut self, sort_order: SortOrder) -> &mut Self {
419        self.sort_orders.push(sort_order);
420        self
421    }
422
423    pub fn paginate(&mut self, page: u64, page_size: u64) -> &mut Self {
424        self.pagination = Some(Pagination::new(page, page_size));
425        self
426    }
427
428    pub fn distinct(&mut self) -> &mut Self {
429        self.distinct = true;
430        self
431    }
432
433    pub fn group_by(&mut self, columns: Vec<&str>) -> &mut Self {
434        self.group_by_columns = Some(columns.iter().map(|s| s.to_string()).collect());
435        self
436    }
437
438    pub fn inner_join(&mut self, table_name: &str, on_column: &str, referenced_column: &str) -> &mut Self {
439        self.joins.push(JoinClause {
440            join_type: JoinType::Inner,
441            table_name: table_name.to_string(),
442            on_column: on_column.to_string(),
443            referenced_column: referenced_column.to_string(),
444        });
445        self
446    }
447
448    pub fn left_join(&mut self, table_name: &str, on_column: &str, referenced_column: &str) -> &mut Self {
449        self.joins.push(JoinClause {
450            join_type: JoinType::Left,
451            table_name: table_name.to_string(),
452            on_column: on_column.to_string(),
453            referenced_column: referenced_column.to_string(),
454        });
455        self
456    }
457
458    pub fn build(&self) -> (String, Vec<serde_json::Value>) {
459        let mut sql = String::new();
460        let mut params = Vec::new();
461
462        sql.push_str("SELECT ");
463
464        if self.distinct {
465            sql.push_str("DISTINCT ");
466        }
467
468        if let Some(columns) = &self.select_columns {
469            sql.push_str(&columns.join(", "));
470        } else {
471            sql.push_str("*");
472        }
473
474        sql.push_str(&format!(" FROM {}", self.table_name));
475
476        for join in &self.joins {
477            let join_type = match join.join_type {
478                JoinType::Inner => "INNER JOIN",
479                JoinType::Left => "LEFT JOIN",
480                JoinType::Right => "RIGHT JOIN",
481                JoinType::Full => "FULL JOIN",
482            };
483            sql.push_str(&format!(
484                " {} {} ON {}.{} = {}.{}",
485                join_type, join.table_name, self.table_name, join.on_column, join.table_name, join.referenced_column
486            ));
487        }
488
489        if !self.criteria.is_empty() {
490            sql.push_str(" WHERE 1=1");
491            for criteria in &self.criteria {
492                sql.push_str(" AND ");
493                let (clause, values) = criteria.to_sql();
494                sql.push_str(&clause);
495                params.extend(values);
496            }
497        }
498
499        if let Some(group_by) = &self.group_by_columns {
500            sql.push_str(&format!(" GROUP BY {}", group_by.join(", ")));
501        }
502
503        if !self.having_criteria.is_empty() {
504            sql.push_str(" HAVING 1=1");
505            for criteria in &self.having_criteria {
506                sql.push_str(" AND ");
507                let (clause, values) = criteria.to_sql();
508                sql.push_str(&clause);
509                params.extend(values);
510            }
511        }
512
513        if !self.sort_orders.is_empty() {
514            let orders: Vec<String> = self.sort_orders.iter()
515                .map(|o| format!("{} {}", o.column, if o.ascending { "ASC" } else { "DESC" }))
516                .collect();
517            sql.push_str(&format!(" ORDER BY {}", orders.join(", ")));
518        }
519
520        if let Some(pagination) = &self.pagination {
521            sql.push_str(&format!(" LIMIT {} OFFSET {}", pagination.limit(), pagination.offset()));
522        }
523
524        (sql, params)
525    }
526}