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
49impl TableDef {
50 pub fn new(name: &str, struct_name: &str) -> Self {
52 Self {
53 name: name.to_string(),
54 schema: None,
55 struct_name: struct_name.to_string(),
56 fields: Vec::new(),
57 indexes: Vec::new(),
58 composite_indexes: Vec::new(),
59 soft_delete: false,
60 tenant_field: None,
61 doc: None,
62 }
63 }
64
65 pub fn to_typescript_interface(&self) -> String {
67 let fields: Vec<String> = self.fields.iter().map(|f| f.to_typescript()).collect();
68
69 format!(
70 "export interface {} {{\n{}\n}}",
71 self.struct_name,
72 fields.join("\n")
73 )
74 }
75
76 pub fn qualified_name(&self) -> String {
78 match &self.schema {
79 Some(schema) => format!("{}.{}", schema, self.name),
80 None => self.name.clone(),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct IndexDef {
88 pub name: String,
90
91 pub column: String,
93
94 pub index_type: IndexType,
96
97 pub unique: bool,
99
100 pub where_clause: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct CompositeIndexDef {
107 pub name: Option<String>,
109
110 pub columns: Vec<String>,
112
113 pub orders: Vec<IndexOrder>,
115
116 pub index_type: IndexType,
118
119 pub unique: bool,
121
122 pub where_clause: Option<String>,
124}
125
126impl CompositeIndexDef {
127 pub fn to_sql(&self, table_name: &str) -> String {
129 let name = self
130 .name
131 .clone()
132 .unwrap_or_else(|| format!("idx_{}_{}", table_name, self.columns.join("_")));
133
134 let columns: Vec<String> = self
135 .columns
136 .iter()
137 .zip(self.orders.iter())
138 .map(|(col, order)| match order {
139 IndexOrder::Asc => col.clone(),
140 IndexOrder::Desc => format!("{} DESC", col),
141 })
142 .collect();
143
144 let unique = if self.unique { "UNIQUE " } else { "" };
145 let using = match self.index_type {
146 IndexType::Btree => "",
147 IndexType::Hash => " USING HASH",
148 IndexType::Gin => " USING GIN",
149 IndexType::Gist => " USING GIST",
150 };
151
152 let mut sql = format!(
153 "CREATE {}INDEX {}{} ON {}({});",
154 unique,
155 name,
156 using,
157 table_name,
158 columns.join(", ")
159 );
160
161 if let Some(ref where_clause) = self.where_clause {
162 sql = sql.trim_end_matches(';').to_string();
163 sql.push_str(&format!(" WHERE {};", where_clause));
164 }
165
166 sql
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
172pub enum IndexType {
173 #[default]
174 Btree,
175 Hash,
176 Gin,
177 Gist,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
182pub enum IndexOrder {
183 #[default]
184 Asc,
185 Desc,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub enum RelationType {
191 BelongsTo { target: String, foreign_key: String },
193 HasMany { target: String, foreign_key: String },
195 HasOne { target: String, foreign_key: String },
197 ManyToMany { target: String, through: String },
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::schema::types::RustType;
205
206 #[test]
207 fn test_table_def_basic() {
208 let mut table = TableDef::new("users", "User");
209 table.fields.push(FieldDef::new("id", RustType::Uuid));
210 table.fields.push(FieldDef::new("email", RustType::String));
211 assert_eq!(table.fields.len(), 2);
212 }
213
214 #[test]
215 fn test_table_to_typescript() {
216 let mut table = TableDef::new("users", "User");
217 table.fields.push(FieldDef::new("id", RustType::Uuid));
218 table.fields.push(FieldDef::new("email", RustType::String));
219
220 let ts = table.to_typescript_interface();
221 assert!(ts.contains("export interface User"));
222 assert!(ts.contains("id: string"));
223 assert!(ts.contains("email: string"));
224 }
225
226 #[test]
227 fn test_composite_index_sql() {
228 let idx = CompositeIndexDef {
229 name: Some("idx_tasks_status_priority".to_string()),
230 columns: vec!["status".to_string(), "priority".to_string()],
231 orders: vec![IndexOrder::Asc, IndexOrder::Desc],
232 index_type: IndexType::Btree,
233 unique: false,
234 where_clause: None,
235 };
236
237 let sql = idx.to_sql("tasks");
238 assert_eq!(
239 sql,
240 "CREATE INDEX idx_tasks_status_priority ON tasks(status, priority DESC);"
241 );
242 }
243}