1use crate::theme::Theme;
40
41#[derive(Debug, Clone)]
43pub struct SchemaTreeConfig {
44 pub show_types: bool,
46 pub show_constraints: bool,
48 pub show_indexes: bool,
50 pub show_foreign_keys: bool,
52 pub theme: Option<Theme>,
54 pub use_unicode: bool,
56}
57
58impl Default for SchemaTreeConfig {
59 fn default() -> Self {
60 Self {
61 show_types: true,
62 show_constraints: true,
63 show_indexes: true,
64 show_foreign_keys: true,
65 theme: None,
66 use_unicode: true,
67 }
68 }
69}
70
71impl SchemaTreeConfig {
72 #[must_use]
74 pub fn new() -> Self {
75 Self::default()
76 }
77
78 #[must_use]
80 pub fn show_types(mut self, show: bool) -> Self {
81 self.show_types = show;
82 self
83 }
84
85 #[must_use]
87 pub fn show_constraints(mut self, show: bool) -> Self {
88 self.show_constraints = show;
89 self
90 }
91
92 #[must_use]
94 pub fn show_indexes(mut self, show: bool) -> Self {
95 self.show_indexes = show;
96 self
97 }
98
99 #[must_use]
101 pub fn show_foreign_keys(mut self, show: bool) -> Self {
102 self.show_foreign_keys = show;
103 self
104 }
105
106 #[must_use]
108 pub fn theme(mut self, theme: Theme) -> Self {
109 self.theme = Some(theme);
110 self
111 }
112
113 #[must_use]
115 pub fn ascii(mut self) -> Self {
116 self.use_unicode = false;
117 self
118 }
119
120 #[must_use]
122 pub fn unicode(mut self) -> Self {
123 self.use_unicode = true;
124 self
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct TableData {
131 pub name: String,
133 pub columns: Vec<ColumnData>,
135 pub primary_key: Vec<String>,
137 pub foreign_keys: Vec<ForeignKeyData>,
139 pub indexes: Vec<IndexData>,
141}
142
143#[derive(Debug, Clone)]
145pub struct ColumnData {
146 pub name: String,
148 pub sql_type: String,
150 pub nullable: bool,
152 pub default: Option<String>,
154 pub primary_key: bool,
156 pub auto_increment: bool,
158}
159
160#[derive(Debug, Clone)]
162pub struct ForeignKeyData {
163 pub name: Option<String>,
165 pub column: String,
167 pub foreign_table: String,
169 pub foreign_column: String,
171 pub on_delete: Option<String>,
173 pub on_update: Option<String>,
175}
176
177#[derive(Debug, Clone)]
179pub struct IndexData {
180 pub name: String,
182 pub columns: Vec<String>,
184 pub unique: bool,
186}
187
188#[derive(Debug, Clone)]
192pub struct SchemaTree {
193 tables: Vec<TableData>,
195 config: SchemaTreeConfig,
197}
198
199impl SchemaTree {
200 #[must_use]
202 pub fn new(tables: &[TableData]) -> Self {
203 Self {
204 tables: tables.to_vec(),
205 config: SchemaTreeConfig::default(),
206 }
207 }
208
209 #[must_use]
211 pub fn empty() -> Self {
212 Self {
213 tables: Vec::new(),
214 config: SchemaTreeConfig::default(),
215 }
216 }
217
218 #[must_use]
220 pub fn add_table(mut self, table: TableData) -> Self {
221 self.tables.push(table);
222 self
223 }
224
225 #[must_use]
227 pub fn config(mut self, config: SchemaTreeConfig) -> Self {
228 self.config = config;
229 self
230 }
231
232 #[must_use]
234 pub fn theme(mut self, theme: Theme) -> Self {
235 self.config.theme = Some(theme);
236 self
237 }
238
239 #[must_use]
241 pub fn ascii(mut self) -> Self {
242 self.config.use_unicode = false;
243 self
244 }
245
246 #[must_use]
248 pub fn unicode(mut self) -> Self {
249 self.config.use_unicode = true;
250 self
251 }
252
253 fn chars(&self) -> (&'static str, &'static str, &'static str, &'static str) {
255 if self.config.use_unicode {
256 ("├── ", "└── ", "│ ", " ")
257 } else {
258 ("+-- ", "\\-- ", "| ", " ")
259 }
260 }
261
262 #[must_use]
264 pub fn render_plain(&self) -> String {
265 if self.tables.is_empty() {
266 return "Schema: (empty)".to_string();
267 }
268
269 let mut lines = Vec::new();
270 lines.push("Schema".to_string());
271
272 let table_count = self.tables.len();
273 for (i, table) in self.tables.iter().enumerate() {
274 let is_last = i == table_count - 1;
275 self.render_table_plain(table, "", is_last, &mut lines);
276 }
277
278 lines.join("\n")
279 }
280
281 fn render_table_plain(
283 &self,
284 table: &TableData,
285 prefix: &str,
286 is_last: bool,
287 lines: &mut Vec<String>,
288 ) {
289 let (branch, last_branch, vertical, space) = self.chars();
290 let connector = if is_last { last_branch } else { branch };
291
292 let pk_info = if self.config.show_constraints && !table.primary_key.is_empty() {
294 format!(" [PK: {}]", table.primary_key.join(", "))
295 } else {
296 String::new()
297 };
298 lines.push(format!("{prefix}{connector}Table: {}{pk_info}", table.name));
299
300 let child_prefix = if is_last {
301 format!("{prefix}{space}")
302 } else {
303 format!("{prefix}{vertical}")
304 };
305
306 #[allow(clippy::type_complexity)]
308 let mut children: Vec<(&str, Box<dyn Fn(&str, bool, &mut Vec<String>) + '_>)> = Vec::new();
309
310 if !table.columns.is_empty() {
312 let columns = table.columns.clone();
313 children.push((
314 "Columns",
315 Box::new(move |prefix, is_last, lines| {
316 self.render_columns_plain(&columns, prefix, is_last, lines);
317 }),
318 ));
319 }
320
321 if self.config.show_indexes && !table.indexes.is_empty() {
323 let indexes = table.indexes.clone();
324 children.push((
325 "Indexes",
326 Box::new(move |prefix, is_last, lines| {
327 self.render_indexes_plain(&indexes, prefix, is_last, lines);
328 }),
329 ));
330 }
331
332 if self.config.show_foreign_keys && !table.foreign_keys.is_empty() {
334 let fks = table.foreign_keys.clone();
335 children.push((
336 "Foreign Keys",
337 Box::new(move |prefix, is_last, lines| {
338 self.render_fks_plain(&fks, prefix, is_last, lines);
339 }),
340 ));
341 }
342
343 let child_count = children.len();
345 for (i, (label, render_fn)) in children.into_iter().enumerate() {
346 let is_last_child = i == child_count - 1;
347 let section_connector = if is_last_child { last_branch } else { branch };
348 lines.push(format!("{child_prefix}{section_connector}{label}"));
349
350 let section_prefix = if is_last_child {
351 format!("{child_prefix}{space}")
352 } else {
353 format!("{child_prefix}{vertical}")
354 };
355
356 render_fn(§ion_prefix, true, lines);
357 }
358 }
359
360 fn render_columns_plain(
362 &self,
363 columns: &[ColumnData],
364 prefix: &str,
365 _is_last: bool,
366 lines: &mut Vec<String>,
367 ) {
368 let (branch, last_branch, _, _) = self.chars();
369
370 let col_count = columns.len();
371 for (i, col) in columns.iter().enumerate() {
372 let is_last_col = i == col_count - 1;
373 let connector = if is_last_col { last_branch } else { branch };
374
375 let mut parts = vec![col.name.clone()];
376
377 if self.config.show_types {
378 parts.push(col.sql_type.clone());
379 }
380
381 if self.config.show_constraints {
382 let mut constraints: Vec<String> = Vec::new();
383 if col.primary_key {
384 constraints.push("PK".into());
385 }
386 if col.auto_increment {
387 constraints.push("AUTO".into());
388 }
389 if !col.nullable {
390 constraints.push("NOT NULL".into());
391 }
392 if let Some(ref default) = col.default {
393 constraints.push(format!("DEFAULT {default}"));
394 }
395 if !constraints.is_empty() {
396 parts.push(format!("[{}]", constraints.join(", ")));
397 }
398 }
399
400 lines.push(format!("{prefix}{connector}{}", parts.join(" ")));
401 }
402 }
403
404 fn render_indexes_plain(
406 &self,
407 indexes: &[IndexData],
408 prefix: &str,
409 _is_last: bool,
410 lines: &mut Vec<String>,
411 ) {
412 let (branch, last_branch, _, _) = self.chars();
413
414 let idx_count = indexes.len();
415 for (i, idx) in indexes.iter().enumerate() {
416 let is_last_idx = i == idx_count - 1;
417 let connector = if is_last_idx { last_branch } else { branch };
418
419 let unique_marker = if idx.unique { "UNIQUE " } else { "" };
420 lines.push(format!(
421 "{prefix}{connector}{unique_marker}{} ({})",
422 idx.name,
423 idx.columns.join(", ")
424 ));
425 }
426 }
427
428 fn render_fks_plain(
430 &self,
431 fks: &[ForeignKeyData],
432 prefix: &str,
433 _is_last: bool,
434 lines: &mut Vec<String>,
435 ) {
436 let (branch, last_branch, _, _) = self.chars();
437
438 let fk_count = fks.len();
439 for (i, fk) in fks.iter().enumerate() {
440 let is_last_fk = i == fk_count - 1;
441 let connector = if is_last_fk { last_branch } else { branch };
442
443 let name = fk.name.as_deref().unwrap_or("(unnamed)");
444 let mut parts = vec![format!(
445 "{}: {} -> {}.{}",
446 name, fk.column, fk.foreign_table, fk.foreign_column
447 )];
448
449 if let Some(ref on_delete) = fk.on_delete {
450 parts.push(format!("ON DELETE {on_delete}"));
451 }
452 if let Some(ref on_update) = fk.on_update {
453 parts.push(format!("ON UPDATE {on_update}"));
454 }
455
456 lines.push(format!("{prefix}{connector}{}", parts.join(" ")));
457 }
458 }
459
460 #[must_use]
462 pub fn render_styled(&self) -> String {
463 let theme = self.config.theme.clone().unwrap_or_default();
464
465 if self.tables.is_empty() {
466 let dim = theme.dim.color_code();
467 let reset = "\x1b[0m";
468 return format!("{dim}Schema: (empty){reset}");
469 }
470
471 let mut lines = Vec::new();
472 let keyword_color = theme.sql_keyword.color_code();
473 let reset = "\x1b[0m";
474 lines.push(format!("{keyword_color}Schema{reset}"));
475
476 let table_count = self.tables.len();
477 for (i, table) in self.tables.iter().enumerate() {
478 let is_last = i == table_count - 1;
479 self.render_table_styled(table, "", is_last, &mut lines, &theme);
480 }
481
482 lines.join("\n")
483 }
484
485 fn render_table_styled(
487 &self,
488 table: &TableData,
489 prefix: &str,
490 is_last: bool,
491 lines: &mut Vec<String>,
492 theme: &Theme,
493 ) {
494 let (branch, last_branch, vertical, space) = self.chars();
495 let connector = if is_last { last_branch } else { branch };
496
497 let reset = "\x1b[0m";
498 let dim = theme.dim.color_code();
499 let table_color = theme.sql_keyword.color_code();
500 let name_color = theme.sql_identifier.color_code();
501 let pk_color = theme.dim.color_code();
502
503 let pk_info = if self.config.show_constraints && !table.primary_key.is_empty() {
505 format!(" {pk_color}[PK: {}]{reset}", table.primary_key.join(", "))
506 } else {
507 String::new()
508 };
509 lines.push(format!(
510 "{dim}{prefix}{connector}{reset}{table_color}Table:{reset} {name_color}{}{reset}{pk_info}",
511 table.name
512 ));
513
514 let child_prefix = if is_last {
515 format!("{prefix}{space}")
516 } else {
517 format!("{prefix}{vertical}")
518 };
519
520 #[allow(clippy::type_complexity)]
522 let mut sections: Vec<(
523 &str,
524 Box<dyn Fn(&str, bool, &mut Vec<String>, &Theme) + '_>,
525 )> = Vec::new();
526
527 if !table.columns.is_empty() {
528 let columns = table.columns.clone();
529 sections.push((
530 "Columns",
531 Box::new(move |prefix, is_last, lines, theme| {
532 self.render_columns_styled(&columns, prefix, is_last, lines, theme);
533 }),
534 ));
535 }
536
537 if self.config.show_indexes && !table.indexes.is_empty() {
538 let indexes = table.indexes.clone();
539 sections.push((
540 "Indexes",
541 Box::new(move |prefix, is_last, lines, theme| {
542 self.render_indexes_styled(&indexes, prefix, is_last, lines, theme);
543 }),
544 ));
545 }
546
547 if self.config.show_foreign_keys && !table.foreign_keys.is_empty() {
548 let fks = table.foreign_keys.clone();
549 sections.push((
550 "Foreign Keys",
551 Box::new(move |prefix, is_last, lines, theme| {
552 self.render_fks_styled(&fks, prefix, is_last, lines, theme);
553 }),
554 ));
555 }
556
557 let section_count = sections.len();
558 for (i, (label, render_fn)) in sections.into_iter().enumerate() {
559 let is_last_section = i == section_count - 1;
560 let section_connector = if is_last_section { last_branch } else { branch };
561 let header_color = theme.sql_keyword.color_code();
562 lines.push(format!(
563 "{dim}{child_prefix}{section_connector}{reset}{header_color}{label}{reset}"
564 ));
565
566 let section_prefix = if is_last_section {
567 format!("{child_prefix}{space}")
568 } else {
569 format!("{child_prefix}{vertical}")
570 };
571
572 render_fn(§ion_prefix, true, lines, theme);
573 }
574 }
575
576 fn render_columns_styled(
578 &self,
579 columns: &[ColumnData],
580 prefix: &str,
581 _is_last: bool,
582 lines: &mut Vec<String>,
583 theme: &Theme,
584 ) {
585 let (branch, last_branch, _, _) = self.chars();
586 let reset = "\x1b[0m";
587 let dim = theme.dim.color_code();
588 let name_color = theme.sql_identifier.color_code();
589 let type_color = theme.sql_keyword.color_code();
590 let constraint_color = theme.dim.color_code();
591
592 let col_count = columns.len();
593 for (i, col) in columns.iter().enumerate() {
594 let is_last_col = i == col_count - 1;
595 let connector = if is_last_col { last_branch } else { branch };
596
597 let mut line = format!(
598 "{dim}{prefix}{connector}{reset}{name_color}{}{reset}",
599 col.name
600 );
601
602 if self.config.show_types {
603 line.push_str(&format!(" {type_color}{}{reset}", col.sql_type));
604 }
605
606 if self.config.show_constraints {
607 let mut constraints: Vec<String> = Vec::new();
608 if col.primary_key {
609 constraints.push("PK".into());
610 }
611 if col.auto_increment {
612 constraints.push("AUTO".into());
613 }
614 if !col.nullable {
615 constraints.push("NOT NULL".into());
616 }
617 if let Some(ref default) = col.default {
618 constraints.push(format!("DEFAULT {default}"));
619 }
620 if !constraints.is_empty() {
621 line.push_str(&format!(
622 " {constraint_color}[{}]{reset}",
623 constraints.join(", ")
624 ));
625 }
626 }
627
628 lines.push(line);
629 }
630 }
631
632 fn render_indexes_styled(
634 &self,
635 indexes: &[IndexData],
636 prefix: &str,
637 _is_last: bool,
638 lines: &mut Vec<String>,
639 theme: &Theme,
640 ) {
641 let (branch, last_branch, _, _) = self.chars();
642 let reset = "\x1b[0m";
643 let dim = theme.dim.color_code();
644 let name_color = theme.sql_identifier.color_code();
645 let keyword_color = theme.sql_keyword.color_code();
646
647 let idx_count = indexes.len();
648 for (i, idx) in indexes.iter().enumerate() {
649 let is_last_idx = i == idx_count - 1;
650 let connector = if is_last_idx { last_branch } else { branch };
651
652 let unique_marker = if idx.unique {
653 format!("{keyword_color}UNIQUE {reset}")
654 } else {
655 String::new()
656 };
657
658 lines.push(format!(
659 "{dim}{prefix}{connector}{reset}{unique_marker}{name_color}{}{reset} ({dim}{}{reset})",
660 idx.name,
661 idx.columns.join(", ")
662 ));
663 }
664 }
665
666 fn render_fks_styled(
668 &self,
669 fks: &[ForeignKeyData],
670 prefix: &str,
671 _is_last: bool,
672 lines: &mut Vec<String>,
673 theme: &Theme,
674 ) {
675 let (branch, last_branch, _, _) = self.chars();
676 let reset = "\x1b[0m";
677 let dim = theme.dim.color_code();
678 let name_color = theme.sql_identifier.color_code();
679 let ref_color = theme.string_value.color_code();
680
681 let fk_count = fks.len();
682 for (i, fk) in fks.iter().enumerate() {
683 let is_last_fk = i == fk_count - 1;
684 let connector = if is_last_fk { last_branch } else { branch };
685
686 let name = fk.name.as_deref().unwrap_or("(unnamed)");
687
688 let mut line = format!(
689 "{dim}{prefix}{connector}{reset}{name_color}{name}{reset}: {dim}{}{reset} -> {ref_color}{}.{}{reset}",
690 fk.column, fk.foreign_table, fk.foreign_column
691 );
692
693 if let Some(ref on_delete) = fk.on_delete {
694 line.push_str(&format!(" {dim}ON DELETE {on_delete}{reset}"));
695 }
696 if let Some(ref on_update) = fk.on_update {
697 line.push_str(&format!(" {dim}ON UPDATE {on_update}{reset}"));
698 }
699
700 lines.push(line);
701 }
702 }
703
704 #[must_use]
706 pub fn to_json(&self) -> serde_json::Value {
707 let tables: Vec<serde_json::Value> = self.tables.iter().map(Self::table_to_json).collect();
708
709 serde_json::json!({
710 "schema": {
711 "tables": tables
712 }
713 })
714 }
715
716 fn table_to_json(table: &TableData) -> serde_json::Value {
718 let columns: Vec<serde_json::Value> = table
719 .columns
720 .iter()
721 .map(|col| {
722 serde_json::json!({
723 "name": col.name,
724 "type": col.sql_type,
725 "nullable": col.nullable,
726 "default": col.default,
727 "primary_key": col.primary_key,
728 "auto_increment": col.auto_increment,
729 })
730 })
731 .collect();
732
733 let indexes: Vec<serde_json::Value> = table
734 .indexes
735 .iter()
736 .map(|idx| {
737 serde_json::json!({
738 "name": idx.name,
739 "columns": idx.columns,
740 "unique": idx.unique,
741 })
742 })
743 .collect();
744
745 let foreign_keys: Vec<serde_json::Value> = table
746 .foreign_keys
747 .iter()
748 .map(|fk| {
749 serde_json::json!({
750 "name": fk.name,
751 "column": fk.column,
752 "foreign_table": fk.foreign_table,
753 "foreign_column": fk.foreign_column,
754 "on_delete": fk.on_delete,
755 "on_update": fk.on_update,
756 })
757 })
758 .collect();
759
760 serde_json::json!({
761 "name": table.name,
762 "columns": columns,
763 "primary_key": table.primary_key,
764 "indexes": indexes,
765 "foreign_keys": foreign_keys,
766 })
767 }
768}
769
770impl Default for SchemaTree {
771 fn default() -> Self {
772 Self::empty()
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779
780 fn sample_column(name: &str, sql_type: &str, primary_key: bool) -> ColumnData {
781 ColumnData {
782 name: name.to_string(),
783 sql_type: sql_type.to_string(),
784 nullable: !primary_key,
785 default: None,
786 primary_key,
787 auto_increment: primary_key,
788 }
789 }
790
791 fn sample_table() -> TableData {
792 TableData {
793 name: "heroes".to_string(),
794 columns: vec![
795 sample_column("id", "INTEGER", true),
796 sample_column("name", "TEXT", false),
797 sample_column("secret_name", "TEXT", false),
798 ],
799 primary_key: vec!["id".to_string()],
800 foreign_keys: vec![],
801 indexes: vec![],
802 }
803 }
804
805 fn sample_table_with_fk() -> TableData {
806 TableData {
807 name: "team_members".to_string(),
808 columns: vec![
809 sample_column("id", "INTEGER", true),
810 sample_column("hero_id", "INTEGER", false),
811 sample_column("team_id", "INTEGER", false),
812 ],
813 primary_key: vec!["id".to_string()],
814 foreign_keys: vec![
815 ForeignKeyData {
816 name: Some("fk_hero".to_string()),
817 column: "hero_id".to_string(),
818 foreign_table: "heroes".to_string(),
819 foreign_column: "id".to_string(),
820 on_delete: Some("CASCADE".to_string()),
821 on_update: None,
822 },
823 ForeignKeyData {
824 name: Some("fk_team".to_string()),
825 column: "team_id".to_string(),
826 foreign_table: "teams".to_string(),
827 foreign_column: "id".to_string(),
828 on_delete: Some("SET NULL".to_string()),
829 on_update: Some("CASCADE".to_string()),
830 },
831 ],
832 indexes: vec![IndexData {
833 name: "idx_hero_team".to_string(),
834 columns: vec!["hero_id".to_string(), "team_id".to_string()],
835 unique: true,
836 }],
837 }
838 }
839
840 #[test]
841 fn test_empty_schema() {
842 let tree = SchemaTree::empty();
843 let output = tree.render_plain();
844 assert_eq!(output, "Schema: (empty)");
845 }
846
847 #[test]
848 fn test_schema_tree_new() {
849 let tree = SchemaTree::new(&[sample_table()]);
850 let output = tree.render_plain();
851 assert!(output.contains("Schema"));
852 assert!(output.contains("Table: heroes"));
853 }
854
855 #[test]
856 fn test_schema_tree_columns() {
857 let tree = SchemaTree::new(&[sample_table()]);
858 let output = tree.render_plain();
859 assert!(output.contains("Columns"));
860 assert!(output.contains("id INTEGER"));
861 assert!(output.contains("name TEXT"));
862 assert!(output.contains("secret_name TEXT"));
863 }
864
865 #[test]
866 fn test_schema_tree_primary_key() {
867 let tree = SchemaTree::new(&[sample_table()]);
868 let output = tree.render_plain();
869 assert!(output.contains("[PK: id]"));
870 assert!(output.contains("[PK, AUTO, NOT NULL]"));
871 }
872
873 #[test]
874 fn test_schema_tree_indexes() {
875 let tree = SchemaTree::new(&[sample_table_with_fk()]);
876 let output = tree.render_plain();
877 assert!(output.contains("Indexes"));
878 assert!(output.contains("UNIQUE idx_hero_team"));
879 assert!(output.contains("hero_id, team_id"));
880 }
881
882 #[test]
883 fn test_schema_tree_foreign_keys() {
884 let tree = SchemaTree::new(&[sample_table_with_fk()]);
885 let output = tree.render_plain();
886 assert!(output.contains("Foreign Keys"));
887 assert!(output.contains("fk_hero: hero_id -> heroes.id"));
888 assert!(output.contains("ON DELETE CASCADE"));
889 assert!(output.contains("fk_team: team_id -> teams.id"));
890 assert!(output.contains("ON UPDATE CASCADE"));
891 }
892
893 #[test]
894 fn test_schema_tree_unicode() {
895 let tree = SchemaTree::new(&[sample_table()]).unicode();
896 let output = tree.render_plain();
897 assert!(output.contains("├── ") || output.contains("└── "));
898 }
899
900 #[test]
901 fn test_schema_tree_ascii() {
902 let tree = SchemaTree::new(&[sample_table()]).ascii();
903 let output = tree.render_plain();
904 assert!(output.contains("+-- ") || output.contains("\\-- "));
905 }
906
907 #[test]
908 fn test_schema_tree_styled_contains_ansi() {
909 let tree = SchemaTree::new(&[sample_table()]);
910 let styled = tree.render_styled();
911 assert!(styled.contains('\x1b'));
912 }
913
914 #[test]
915 fn test_schema_tree_config_no_types() {
916 let config = SchemaTreeConfig::new().show_types(false);
917 let tree = SchemaTree::new(&[sample_table()]).config(config);
918 let output = tree.render_plain();
919 assert!(output.contains("id"));
920 assert!(!output.contains("INTEGER"));
921 }
922
923 #[test]
924 fn test_schema_tree_config_no_constraints() {
925 let config = SchemaTreeConfig::new().show_constraints(false);
926 let tree = SchemaTree::new(&[sample_table()]).config(config);
927 let output = tree.render_plain();
928 assert!(!output.contains("[PK"));
929 assert!(!output.contains("NOT NULL"));
930 }
931
932 #[test]
933 fn test_schema_tree_config_no_indexes() {
934 let config = SchemaTreeConfig::new().show_indexes(false);
935 let tree = SchemaTree::new(&[sample_table_with_fk()]).config(config);
936 let output = tree.render_plain();
937 assert!(!output.contains("Indexes"));
938 }
939
940 #[test]
941 fn test_schema_tree_config_no_fks() {
942 let config = SchemaTreeConfig::new().show_foreign_keys(false);
943 let tree = SchemaTree::new(&[sample_table_with_fk()]).config(config);
944 let output = tree.render_plain();
945 assert!(!output.contains("Foreign Keys"));
946 }
947
948 #[test]
949 fn test_schema_tree_to_json() {
950 let tree = SchemaTree::new(&[sample_table()]);
951 let json = tree.to_json();
952 assert!(json["schema"]["tables"].is_array());
953 assert_eq!(json["schema"]["tables"][0]["name"], "heroes");
954 assert!(json["schema"]["tables"][0]["columns"].is_array());
955 }
956
957 #[test]
958 fn test_schema_tree_multiple_tables() {
959 let tree = SchemaTree::new(&[sample_table(), sample_table_with_fk()]);
960 let output = tree.render_plain();
961 assert!(output.contains("Table: heroes"));
962 assert!(output.contains("Table: team_members"));
963 }
964
965 #[test]
966 fn test_schema_tree_add_table() {
967 let tree = SchemaTree::empty().add_table(sample_table());
968 let output = tree.render_plain();
969 assert!(output.contains("Table: heroes"));
970 }
971
972 #[test]
973 fn test_default() {
974 let tree = SchemaTree::default();
975 let output = tree.render_plain();
976 assert!(output.contains("(empty)"));
977 }
978
979 #[test]
980 fn test_column_with_default() {
981 let mut table = sample_table();
982 table.columns.push(ColumnData {
983 name: "status".to_string(),
984 sql_type: "TEXT".to_string(),
985 nullable: true,
986 default: Some("'active'".to_string()),
987 primary_key: false,
988 auto_increment: false,
989 });
990
991 let tree = SchemaTree::new(&[table]);
992 let output = tree.render_plain();
993 assert!(output.contains("DEFAULT 'active'"));
994 }
995}