1use serde::{Deserialize, Serialize};
2
3use super::field::FieldDef;
4
5pub trait ModelMeta: Sized {
8 const TABLE_NAME: &'static str;
10
11 fn table_def() -> TableDef;
13
14 fn primary_key_field() -> &'static str;
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TableDef {
21 pub name: String,
23
24 pub schema: Option<String>,
26
27 pub struct_name: String,
29
30 pub fields: Vec<FieldDef>,
32
33 pub indexes: Vec<IndexDef>,
35
36 pub composite_indexes: Vec<CompositeIndexDef>,
38
39 pub soft_delete: bool,
41
42 pub tenant_field: Option<String>,
44
45 pub doc: Option<String>,
47
48 #[serde(default)]
50 pub is_dto: bool,
51}
52
53impl TableDef {
54 pub fn new(name: &str, struct_name: &str) -> Self {
56 Self {
57 name: name.to_string(),
58 schema: None,
59 struct_name: struct_name.to_string(),
60 fields: Vec::new(),
61 indexes: Vec::new(),
62 composite_indexes: Vec::new(),
63 soft_delete: false,
64 tenant_field: None,
65 doc: None,
66 is_dto: false,
67 }
68 }
69
70 pub fn qualified_name(&self) -> String {
72 match &self.schema {
73 Some(schema) => format!("{}.{}", schema, self.name),
74 None => self.name.clone(),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct IndexDef {
82 pub name: String,
84
85 pub column: String,
87
88 pub index_type: IndexType,
90
91 pub unique: bool,
93
94 pub where_clause: Option<String>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct CompositeIndexDef {
101 pub name: Option<String>,
103
104 pub columns: Vec<String>,
106
107 pub orders: Vec<IndexOrder>,
109
110 pub index_type: IndexType,
112
113 pub unique: bool,
115
116 pub where_clause: Option<String>,
118}
119
120impl CompositeIndexDef {
121 pub fn to_sql(&self, table_name: &str) -> String {
123 let name = self
124 .name
125 .clone()
126 .unwrap_or_else(|| format!("idx_{}_{}", table_name, self.columns.join("_")));
127
128 let columns: Vec<String> = self
129 .columns
130 .iter()
131 .zip(self.orders.iter())
132 .map(|(col, order)| match order {
133 IndexOrder::Asc => col.clone(),
134 IndexOrder::Desc => format!("{} DESC", col),
135 })
136 .collect();
137
138 let unique = if self.unique { "UNIQUE " } else { "" };
139 let using = match self.index_type {
140 IndexType::Btree => "",
141 IndexType::Hash => " USING HASH",
142 IndexType::Gin => " USING GIN",
143 IndexType::Gist => " USING GIST",
144 };
145
146 let mut sql = format!(
147 "CREATE {}INDEX {}{} ON {}({});",
148 unique,
149 name,
150 using,
151 table_name,
152 columns.join(", ")
153 );
154
155 if let Some(ref where_clause) = self.where_clause {
156 sql = sql.trim_end_matches(';').to_string();
157 sql.push_str(&format!(" WHERE {};", where_clause));
158 }
159
160 sql
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
166pub enum IndexType {
167 #[default]
168 Btree,
169 Hash,
170 Gin,
171 Gist,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
176pub enum IndexOrder {
177 #[default]
178 Asc,
179 Desc,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub enum RelationType {
185 BelongsTo { target: String, foreign_key: String },
187 HasMany { target: String, foreign_key: String },
189 HasOne { target: String, foreign_key: String },
191 ManyToMany { target: String, through: String },
193}
194
195#[cfg(test)]
196#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
197mod tests {
198 use super::*;
199 use crate::schema::types::RustType;
200
201 #[test]
202 fn test_table_def_basic() {
203 let mut table = TableDef::new("users", "User");
204 table.fields.push(FieldDef::new("id", RustType::Uuid));
205 table.fields.push(FieldDef::new("email", RustType::String));
206 assert_eq!(table.fields.len(), 2);
207 }
208
209 #[test]
210 fn test_composite_index_sql() {
211 let idx = CompositeIndexDef {
212 name: Some("idx_tasks_status_priority".to_string()),
213 columns: vec!["status".to_string(), "priority".to_string()],
214 orders: vec![IndexOrder::Asc, IndexOrder::Desc],
215 index_type: IndexType::Btree,
216 unique: false,
217 where_clause: None,
218 };
219
220 let sql = idx.to_sql("tasks");
221 assert_eq!(
222 sql,
223 "CREATE INDEX idx_tasks_status_priority ON tasks(status, priority DESC);"
224 );
225 }
226}