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 to_typescript_interface(&self) -> String {
72 let fields: Vec<String> = self.fields.iter().map(|f| f.to_typescript()).collect();
73
74 format!(
75 "export interface {} {{\n{}\n}}",
76 self.struct_name,
77 fields.join("\n")
78 )
79 }
80
81 pub fn qualified_name(&self) -> String {
83 match &self.schema {
84 Some(schema) => format!("{}.{}", schema, self.name),
85 None => self.name.clone(),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct IndexDef {
93 pub name: String,
95
96 pub column: String,
98
99 pub index_type: IndexType,
101
102 pub unique: bool,
104
105 pub where_clause: Option<String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CompositeIndexDef {
112 pub name: Option<String>,
114
115 pub columns: Vec<String>,
117
118 pub orders: Vec<IndexOrder>,
120
121 pub index_type: IndexType,
123
124 pub unique: bool,
126
127 pub where_clause: Option<String>,
129}
130
131impl CompositeIndexDef {
132 pub fn to_sql(&self, table_name: &str) -> String {
134 let name = self
135 .name
136 .clone()
137 .unwrap_or_else(|| format!("idx_{}_{}", table_name, self.columns.join("_")));
138
139 let columns: Vec<String> = self
140 .columns
141 .iter()
142 .zip(self.orders.iter())
143 .map(|(col, order)| match order {
144 IndexOrder::Asc => col.clone(),
145 IndexOrder::Desc => format!("{} DESC", col),
146 })
147 .collect();
148
149 let unique = if self.unique { "UNIQUE " } else { "" };
150 let using = match self.index_type {
151 IndexType::Btree => "",
152 IndexType::Hash => " USING HASH",
153 IndexType::Gin => " USING GIN",
154 IndexType::Gist => " USING GIST",
155 };
156
157 let mut sql = format!(
158 "CREATE {}INDEX {}{} ON {}({});",
159 unique,
160 name,
161 using,
162 table_name,
163 columns.join(", ")
164 );
165
166 if let Some(ref where_clause) = self.where_clause {
167 sql = sql.trim_end_matches(';').to_string();
168 sql.push_str(&format!(" WHERE {};", where_clause));
169 }
170
171 sql
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
177pub enum IndexType {
178 #[default]
179 Btree,
180 Hash,
181 Gin,
182 Gist,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub enum IndexOrder {
188 #[default]
189 Asc,
190 Desc,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub enum RelationType {
196 BelongsTo { target: String, foreign_key: String },
198 HasMany { target: String, foreign_key: String },
200 HasOne { target: String, foreign_key: String },
202 ManyToMany { target: String, through: String },
204}
205
206#[cfg(test)]
207#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
208mod tests {
209 use super::*;
210 use crate::schema::types::RustType;
211
212 #[test]
213 fn test_table_def_basic() {
214 let mut table = TableDef::new("users", "User");
215 table.fields.push(FieldDef::new("id", RustType::Uuid));
216 table.fields.push(FieldDef::new("email", RustType::String));
217 assert_eq!(table.fields.len(), 2);
218 }
219
220 #[test]
221 fn test_table_to_typescript() {
222 let mut table = TableDef::new("users", "User");
223 table.fields.push(FieldDef::new("id", RustType::Uuid));
224 table.fields.push(FieldDef::new("email", RustType::String));
225
226 let ts = table.to_typescript_interface();
227 assert!(ts.contains("export interface User"));
228 assert!(ts.contains("id: string"));
229 assert!(ts.contains("email: string"));
230 }
231
232 #[test]
233 fn test_composite_index_sql() {
234 let idx = CompositeIndexDef {
235 name: Some("idx_tasks_status_priority".to_string()),
236 columns: vec!["status".to_string(), "priority".to_string()],
237 orders: vec![IndexOrder::Asc, IndexOrder::Desc],
238 index_type: IndexType::Btree,
239 unique: false,
240 where_clause: None,
241 };
242
243 let sql = idx.to_sql("tasks");
244 assert_eq!(
245 sql,
246 "CREATE INDEX idx_tasks_status_priority ON tasks(status, priority DESC);"
247 );
248 }
249}