1use once_cell::sync::Lazy;
6use serde::{Deserialize, Serialize};
7use std::{collections::HashMap, fs, path::Path, sync::Mutex};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum DatabaseType {
12 Turso,
14 Postgres,
16 MySql,
18}
19
20impl DatabaseType {
21 pub fn current() -> Self {
23 #[cfg(feature = "postgres")]
24 {
25 return DatabaseType::Postgres;
26 }
27 #[cfg(feature = "mysql")]
28 {
29 return DatabaseType::MySql;
30 }
31 #[cfg(all(not(feature = "postgres"), not(feature = "mysql")))]
32 {
33 return DatabaseType::Turso;
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum ColumnType {
41 Integer,
43 Real,
45 Text,
47 Blob,
49}
50
51impl ColumnType {
52 pub fn to_sql(&self) -> &'static str {
54 self.to_sql_for(DatabaseType::Turso)
55 }
56
57 pub fn to_sql_for(&self, db_type: DatabaseType) -> &'static str {
59 match db_type {
60 DatabaseType::Turso => match self {
61 ColumnType::Integer => "INTEGER",
62 ColumnType::Real => "REAL",
63 ColumnType::Text => "TEXT",
64 ColumnType::Blob => "BLOB",
65 },
66 DatabaseType::Postgres => match self {
67 ColumnType::Integer => "BIGINT",
68 ColumnType::Real => "DOUBLE PRECISION",
69 ColumnType::Text => "TEXT",
70 ColumnType::Blob => "BYTEA",
71 },
72 DatabaseType::MySql => match self {
73 ColumnType::Integer => "BIGINT",
74 ColumnType::Real => "DOUBLE",
75 ColumnType::Text => "VARCHAR(255)",
76 ColumnType::Blob => "BLOB",
77 },
78 }
79 }
80
81 #[cfg(feature = "mysql")]
83 pub fn to_mysql_sql(&self) -> &'static str {
84 self.to_sql_for(DatabaseType::MySql)
85 }
86
87 pub fn from_sql(sql: &str) -> Self {
89 match sql.to_uppercase().as_str() {
90 "INTEGER" | "INT" | "BIGINT" | "SMALLINT" | "TINYINT" | "SERIAL" | "BIGSERIAL" => ColumnType::Integer,
91 "REAL" | "FLOAT" | "DOUBLE" | "NUMERIC" | "DECIMAL" | "DOUBLE PRECISION" => ColumnType::Real,
92 "TEXT" | "VARCHAR" | "CHAR" | "STRING" | "VARCHAR(255)" | "TEXT[]" => ColumnType::Text,
93 "BLOB" | "BINARY" | "BYTEA" | "LONGBLOB" => ColumnType::Blob,
94 _ => ColumnType::Text,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ColumnDef {
102 pub name: String,
104 pub col_type: ColumnType,
106 pub nullable: bool,
108 pub primary_key: bool,
110 pub auto_increment: bool,
112 pub default_value: Option<String>,
114 pub unique: bool,
116}
117
118impl ColumnDef {
119 pub fn new(name: impl Into<String>, col_type: ColumnType) -> Self {
121 Self {
122 name: name.into(),
123 col_type,
124 nullable: true,
125 primary_key: false,
126 auto_increment: false,
127 default_value: None,
128 unique: false,
129 }
130 }
131
132 pub fn not_null(mut self) -> Self {
134 self.nullable = false;
135 self
136 }
137
138 pub fn primary_key(mut self) -> Self {
140 self.primary_key = true;
141 self.nullable = false;
142 self
143 }
144
145 pub fn auto_increment(mut self) -> Self {
147 self.auto_increment = true;
148 self
149 }
150
151 pub fn default(mut self, value: impl Into<String>) -> Self {
153 self.default_value = Some(value.into());
154 self
155 }
156
157 pub fn unique(mut self) -> Self {
159 self.unique = true;
160 self
161 }
162
163 pub fn to_sql(&self) -> String {
165 self.to_sql_for(DatabaseType::current())
166 }
167
168 pub fn to_sql_for(&self, db_type: DatabaseType) -> String {
170 let mut sql = format!("{} {}", self.name, self.col_type.to_sql_for(db_type));
171
172 if self.primary_key {
173 sql.push_str(" PRIMARY KEY");
174 }
175
176 if self.auto_increment {
177 match db_type {
178 DatabaseType::Turso => sql.push_str(" AUTOINCREMENT"),
179 DatabaseType::Postgres => {
180 if self.primary_key && self.col_type == ColumnType::Integer {
181 sql = format!("{} SERIAL", self.name);
182 sql.push_str(" PRIMARY KEY");
183 }
184 }
185 DatabaseType::MySql => sql.push_str(" AUTO_INCREMENT"),
186 }
187 }
188
189 if !self.nullable && !self.primary_key {
190 sql.push_str(" NOT NULL");
191 }
192
193 if let Some(ref default) = self.default_value {
194 sql.push_str(&format!(" DEFAULT {}", default));
195 }
196
197 if self.unique && !self.primary_key {
198 sql.push_str(" UNIQUE");
199 }
200
201 sql
202 }
203
204 #[cfg(feature = "mysql")]
206 pub fn to_mysql_sql(&self) -> String {
207 self.to_sql_for(DatabaseType::MySql)
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct IndexDef {
214 pub name: String,
216 pub table_name: String,
218 pub columns: Vec<String>,
220 pub unique: bool,
222}
223
224impl IndexDef {
225 pub fn new(name: impl Into<String>, table_name: impl Into<String>, columns: Vec<String>) -> Self {
227 Self { name: name.into(), table_name: table_name.into(), columns, unique: false }
228 }
229
230 pub fn unique(mut self) -> Self {
232 self.unique = true;
233 self
234 }
235
236 pub fn to_create_sql(&self) -> String {
238 self.to_create_sql_for(DatabaseType::current())
239 }
240
241 pub fn to_create_sql_for(&self, db_type: DatabaseType) -> String {
243 let unique_str = if self.unique { "UNIQUE " } else { "" };
244 let columns = self.columns.join(", ");
245 match db_type {
246 DatabaseType::Turso | DatabaseType::Postgres => {
247 format!("CREATE {}INDEX {} ON {} ({})", unique_str, self.name, self.table_name, columns)
248 }
249 DatabaseType::MySql => {
250 format!("CREATE {}INDEX {} ON {} ({})", unique_str, self.name, self.table_name, columns)
251 }
252 }
253 }
254
255 pub fn to_drop_sql(&self) -> String {
257 format!("DROP INDEX IF EXISTS {}", self.name)
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct DatabaseSchema {
264 pub name: String,
266 pub r#type: DatabaseType,
268 pub url: Option<String>,
270 pub env: Option<String>,
272 pub schemas: Vec<TableSchema>,
274}
275
276impl DatabaseSchema {
277 pub fn new(name: impl Into<String>, db_type: DatabaseType) -> Self {
279 Self { name: name.into(), r#type: db_type, url: None, env: None, schemas: Vec::new() }
280 }
281
282 pub fn url(mut self, url: impl Into<String>) -> Self {
284 self.url = Some(url.into());
285 self
286 }
287
288 pub fn env(mut self, env: impl Into<String>) -> Self {
290 self.env = Some(env.into());
291 self
292 }
293
294 pub fn schema(mut self, schema: TableSchema) -> Self {
296 self.schemas.push(schema);
297 self
298 }
299
300 pub fn get_url(&self) -> Result<String, Box<dyn std::error::Error>> {
302 if let Some(url) = &self.url {
303 Ok(url.clone())
304 }
305 else if let Some(env_name) = &self.env {
306 std::env::var(env_name).map_err(|e| format!("Failed to read environment variable {}: {}", env_name, e).into())
307 }
308 else {
309 Err("No database URL or environment variable specified".into())
310 }
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct TableSchema {
317 pub name: String,
319 pub columns: Vec<ColumnDef>,
321 pub indexes: Vec<IndexDef>,
323 pub foreign_keys: Vec<ForeignKeyDef>,
325}
326
327impl TableSchema {
328 pub fn new(name: impl Into<String>) -> Self {
330 Self { name: name.into(), columns: Vec::new(), indexes: Vec::new(), foreign_keys: Vec::new() }
331 }
332
333 pub fn column(mut self, col: ColumnDef) -> Self {
335 self.columns.push(col);
336 self
337 }
338
339 pub fn index(mut self, idx: IndexDef) -> Self {
341 self.indexes.push(idx);
342 self
343 }
344
345 pub fn foreign_key(mut self, fk: ForeignKeyDef) -> Self {
347 self.foreign_keys.push(fk);
348 self
349 }
350
351 pub fn to_drop_sql(&self) -> String {
353 format!("DROP TABLE IF EXISTS {}", self.name)
354 }
355
356 pub fn primary_key(&self) -> Option<&ColumnDef> {
358 self.columns.iter().find(|c| c.primary_key)
359 }
360
361 pub fn get_column(&self, name: &str) -> Option<&ColumnDef> {
363 self.columns.iter().find(|c| c.name == name)
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct ForeignKeyDef {
370 pub name: String,
372 pub column: String,
374 pub ref_table: String,
376 pub ref_column: String,
378 pub on_update: ReferentialAction,
380 pub on_delete: ReferentialAction,
382}
383
384impl ForeignKeyDef {
385 pub fn new(
387 name: impl Into<String>,
388 column: impl Into<String>,
389 ref_table: impl Into<String>,
390 ref_column: impl Into<String>,
391 ) -> Self {
392 Self {
393 name: name.into(),
394 column: column.into(),
395 ref_table: ref_table.into(),
396 ref_column: ref_column.into(),
397 on_update: ReferentialAction::NoAction,
398 on_delete: ReferentialAction::NoAction,
399 }
400 }
401
402 pub fn on_update(mut self, action: ReferentialAction) -> Self {
404 self.on_update = action;
405 self
406 }
407
408 pub fn on_delete(mut self, action: ReferentialAction) -> Self {
410 self.on_delete = action;
411 self
412 }
413
414 pub fn to_constraint_sql(&self) -> String {
416 format!(
417 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}) ON UPDATE {} ON DELETE {}",
418 self.name,
419 self.column,
420 self.ref_table,
421 self.ref_column,
422 self.on_update.to_sql(),
423 self.on_delete.to_sql()
424 )
425 }
426
427 pub fn to_add_sql(&self, table_name: &str) -> String {
429 format!("ALTER TABLE {} ADD {}", table_name, self.to_constraint_sql())
430 }
431
432 pub fn to_drop_sql(&self, table_name: &str) -> String {
434 format!("ALTER TABLE {} DROP CONSTRAINT {}", table_name, self.name)
435 }
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440pub enum ReferentialAction {
441 NoAction,
443 Restrict,
445 Cascade,
447 SetNull,
449 SetDefault,
451}
452
453impl ReferentialAction {
454 pub fn to_sql(&self) -> &'static str {
456 match self {
457 ReferentialAction::NoAction => "NO ACTION",
458 ReferentialAction::Restrict => "RESTRICT",
459 ReferentialAction::Cascade => "CASCADE",
460 ReferentialAction::SetNull => "SET NULL",
461 ReferentialAction::SetDefault => "SET DEFAULT",
462 }
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct SchemaConfig {
469 pub databases: Vec<DatabaseSchema>,
471 pub default_database: Option<String>,
473}
474
475impl SchemaConfig {
476 pub fn new() -> Self {
478 Self { databases: Vec::new(), default_database: None }
479 }
480
481 pub fn database(mut self, database: DatabaseSchema) -> Self {
483 self.databases.push(database);
484 self
485 }
486
487 pub fn default_database(mut self, name: impl Into<String>) -> Self {
489 self.default_database = Some(name.into());
490 self
491 }
492
493 pub fn get_default_database(&self) -> Option<&DatabaseSchema> {
495 if let Some(default_name) = &self.default_database {
496 self.databases.iter().find(|db| db.name == *default_name)
497 }
498 else {
499 self.databases.first()
500 }
501 }
502
503 pub fn get_database(&self, name: &str) -> Option<&DatabaseSchema> {
505 self.databases.iter().find(|db| db.name == name)
506 }
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct DatabaseLinkConfig {
512 pub r#type: DatabaseType,
514 pub url: Option<String>,
516 pub env: Option<String>,
518}
519
520impl DatabaseLinkConfig {
521 pub fn get_url(&self) -> Result<String, Box<dyn std::error::Error>> {
523 if let Some(url) = &self.url {
524 Ok(url.clone())
525 }
526 else if let Some(env_name) = &self.env {
527 std::env::var(env_name).map_err(|e| format!("Failed to read environment variable {}: {}", env_name, e).into())
528 }
529 else {
530 Err("No database URL or environment variable specified".into())
531 }
532 }
533}
534
535pub mod col {
537 use super::{ColumnDef, ColumnType};
538
539 pub fn integer(name: &str) -> ColumnDef {
541 ColumnDef::new(name, ColumnType::Integer)
542 }
543
544 pub fn real(name: &str) -> ColumnDef {
546 ColumnDef::new(name, ColumnType::Real)
547 }
548
549 pub fn text(name: &str) -> ColumnDef {
551 ColumnDef::new(name, ColumnType::Text)
552 }
553
554 pub fn blob(name: &str) -> ColumnDef {
556 ColumnDef::new(name, ColumnType::Blob)
557 }
558
559 pub fn id(name: &str) -> ColumnDef {
561 ColumnDef::new(name, ColumnType::Integer).primary_key().auto_increment()
562 }
563
564 pub fn boolean(name: &str) -> ColumnDef {
566 ColumnDef::new(name, ColumnType::Integer).default("0")
567 }
568
569 pub fn timestamp(name: &str) -> ColumnDef {
571 ColumnDef::new(name, ColumnType::Text)
572 }
573
574 pub fn json(name: &str) -> ColumnDef {
576 ColumnDef::new(name, ColumnType::Text)
577 }
578}
579
580static SCHEMA_REGISTRY: Lazy<Mutex<HashMap<String, TableSchema>>> = Lazy::new(|| Mutex::new(HashMap::new()));
582
583pub fn register_schema(schema: TableSchema) {
585 let mut registry = SCHEMA_REGISTRY.lock().unwrap();
586 registry.insert(schema.name.clone(), schema);
587}
588
589pub fn register_schemas(schemas: Vec<TableSchema>) {
591 let mut registry = SCHEMA_REGISTRY.lock().unwrap();
592 for schema in schemas {
593 registry.insert(schema.name.clone(), schema);
594 }
595}
596
597pub fn get_registered_schemas() -> Vec<TableSchema> {
599 let registry = SCHEMA_REGISTRY.lock().unwrap();
600 registry.values().cloned().collect()
601}
602
603pub fn get_schema(name: &str) -> Option<TableSchema> {
605 let registry = SCHEMA_REGISTRY.lock().unwrap();
606 registry.get(name).cloned()
607}
608
609pub fn clear_schemas() {
611 let mut registry = SCHEMA_REGISTRY.lock().unwrap();
612 registry.clear();
613}
614
615pub fn export_schemas_to_yaml() -> String {
617 let schemas = get_registered_schemas();
618 serde_yaml::to_string(&schemas).unwrap_or_else(|e| format!("# Error: {}", e))
619}
620
621#[cfg(debug_assertions)]
623pub fn export_schemas_to_yaml_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
624 use std::{fs::File, io::Write};
625
626 let yaml = export_schemas_to_yaml();
627 let mut file = File::create(path)?;
628 file.write_all(yaml.as_bytes())?;
629 Ok(())
630}
631
632#[cfg(debug_assertions)]
634pub fn auto_export_schemas() {
635 let path = std::path::Path::new("schemas.yaml");
636 if let Err(e) = export_schemas_to_yaml_file(path) {
637 eprintln!("Warning: Failed to export schemas: {}", e);
638 }
639 else {
640 println!("Schemas exported to: {}", path.display());
641 }
642}
643
644impl TableSchema {
645 pub fn register(self) -> Self {
647 let name = self.name.clone();
648 register_schema(self);
649 get_schema(&name).unwrap()
650 }
651
652 pub fn to_yaml(&self) -> String {
654 serde_yaml::to_string(self).unwrap_or_else(|e| format!("# Error: {}", e))
655 }
656
657 pub fn to_create_sql(&self) -> String {
659 self.to_create_sql_for(DatabaseType::current())
660 }
661
662 pub fn to_create_sql_for(&self, db_type: DatabaseType) -> String {
664 let mut parts: Vec<String> = self.columns.iter().map(|c| c.to_sql_for(db_type)).collect();
665 for fk in &self.foreign_keys {
666 parts.push(fk.to_constraint_sql());
667 }
668 match db_type {
669 DatabaseType::Turso | DatabaseType::Postgres => {
670 format!("CREATE TABLE {} ({})", self.name, parts.join(", "))
671 }
672 DatabaseType::MySql => {
673 format!("CREATE TABLE {} ({}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", self.name, parts.join(", "))
674 }
675 }
676 }
677
678 #[cfg(feature = "mysql")]
680 pub fn to_mysql_create_sql(&self) -> String {
681 self.to_create_sql_for(DatabaseType::MySql)
682 }
683
684 pub fn to_create_indexes_sql(&self) -> Vec<String> {
686 self.to_create_indexes_sql_for(DatabaseType::current())
687 }
688
689 pub fn to_create_indexes_sql_for(&self, db_type: DatabaseType) -> Vec<String> {
691 self.indexes.iter().map(|idx| idx.to_create_sql_for(db_type)).collect()
692 }
693
694 pub fn to_full_create_sql(&self) -> Vec<String> {
696 self.to_full_create_sql_for(DatabaseType::current())
697 }
698
699 pub fn to_full_create_sql_for(&self, db_type: DatabaseType) -> Vec<String> {
701 let mut sqls = vec![self.to_create_sql_for(db_type)];
702 sqls.extend(self.to_create_indexes_sql_for(db_type));
703 sqls
704 }
705}
706
707pub fn load_schema_config_from_yaml(yaml_str: &str) -> Result<SchemaConfig, serde_yaml::Error> {
709 serde_yaml::from_str(yaml_str)
710}
711
712pub fn load_schema_config_from_yaml_file(path: impl AsRef<Path>) -> Result<SchemaConfig, Box<dyn std::error::Error>> {
714 let content = fs::read_to_string(path)?;
715 let config = load_schema_config_from_yaml(&content)?;
716 Ok(config)
717}
718
719pub fn load_schemas_from_yaml(yaml_str: &str) -> Result<Vec<TableSchema>, serde_yaml::Error> {
721 serde_yaml::from_str(yaml_str)
722}
723
724pub fn load_schemas_from_yaml_file(path: impl AsRef<Path>) -> Result<Vec<TableSchema>, Box<dyn std::error::Error>> {
726 let content = fs::read_to_string(path)?;
727 let schemas = load_schemas_from_yaml(&content)?;
728 Ok(schemas)
729}
730
731pub fn load_and_register_schemas_from_yaml_file(path: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
733 let schemas = load_schemas_from_yaml_file(path)?;
734 register_schemas(schemas);
735 Ok(())
736}
737
738pub fn export_schema_config_to_yaml(config: &SchemaConfig) -> String {
740 serde_yaml::to_string(config).unwrap_or_else(|e| format!("# Error: {}", e))
741}
742
743pub fn export_schema_config_to_yaml_file(config: &SchemaConfig, path: impl AsRef<Path>) -> std::io::Result<()> {
745 use std::{fs::File, io::Write};
746
747 let yaml = export_schema_config_to_yaml(config);
748 let mut file = File::create(path)?;
749 file.write_all(yaml.as_bytes())?;
750 Ok(())
751}
752
753pub fn create_schema_config_from_registered(default_db: DatabaseSchema, default_database: Option<String>) -> SchemaConfig {
755 let schemas = get_registered_schemas();
756 let mut default_db = default_db;
757 default_db.schemas = schemas;
758 SchemaConfig { databases: vec![default_db], default_database }
759}
760
761pub fn generate_full_sql_for_registered_schemas() -> Vec<String> {
763 generate_full_sql_for_registered_schemas_for(DatabaseType::current())
764}
765
766pub fn generate_full_sql_for_registered_schemas_for(db_type: DatabaseType) -> Vec<String> {
768 let schemas = get_registered_schemas();
769 let mut all_sql = Vec::new();
770 for schema in schemas {
771 all_sql.extend(schema.to_full_create_sql_for(db_type));
772 }
773 all_sql
774}
775
776#[cfg(debug_assertions)]
778pub fn export_sql_for_all_databases(output_dir: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
779 let output_dir = output_dir.as_ref();
780 fs::create_dir_all(output_dir)?;
781
782 let db_types = [DatabaseType::Turso, DatabaseType::Postgres, DatabaseType::MySql];
783
784 for &db_type in &db_types {
785 let sqls = generate_full_sql_for_registered_schemas_for(db_type);
786 let db_name = match db_type {
787 DatabaseType::Turso => "turso",
788 DatabaseType::Postgres => "postgres",
789 DatabaseType::MySql => "mysql",
790 };
791 let file_path = output_dir.join(format!("schema_{}.sql", db_name));
792 let content = sqls.join(";\n\n") + ";\n";
793 fs::write(&file_path, content)?;
794 println!("Exported SQL for {} to: {}", db_name, file_path.display());
795 }
796
797 Ok(())
798}