Skip to main content

sqlmodel_console/renderables/
schema_tree.rs

1//! Schema tree visualization for database structure display.
2//!
3//! Displays database schema as a tree view for understanding table structure.
4//!
5//! # Example
6//!
7//! ```rust
8//! use sqlmodel_console::renderables::{SchemaTree, SchemaTreeConfig, TableData, ColumnData};
9//!
10//! let table = TableData {
11//!     name: "heroes".to_string(),
12//!     columns: vec![
13//!         ColumnData {
14//!             name: "id".to_string(),
15//!             sql_type: "INTEGER".to_string(),
16//!             nullable: false,
17//!             default: None,
18//!             primary_key: true,
19//!             auto_increment: true,
20//!         },
21//!         ColumnData {
22//!             name: "name".to_string(),
23//!             sql_type: "TEXT".to_string(),
24//!             nullable: false,
25//!             default: None,
26//!             primary_key: false,
27//!             auto_increment: false,
28//!         },
29//!     ],
30//!     primary_key: vec!["id".to_string()],
31//!     foreign_keys: vec![],
32//!     indexes: vec![],
33//! };
34//!
35//! let tree = SchemaTree::new(&[table]);
36//! println!("{}", tree.render_plain());
37//! ```
38
39use crate::theme::Theme;
40
41/// Configuration for schema tree rendering.
42#[derive(Debug, Clone)]
43pub struct SchemaTreeConfig {
44    /// Show column types
45    pub show_types: bool,
46    /// Show constraints (nullable, default, auto_increment)
47    pub show_constraints: bool,
48    /// Show indexes
49    pub show_indexes: bool,
50    /// Show foreign keys
51    pub show_foreign_keys: bool,
52    /// Theme for styled output
53    pub theme: Option<Theme>,
54    /// Use Unicode box drawing characters
55    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    /// Create a new config with default settings.
73    #[must_use]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Set whether to show column types.
79    #[must_use]
80    pub fn show_types(mut self, show: bool) -> Self {
81        self.show_types = show;
82        self
83    }
84
85    /// Set whether to show constraints.
86    #[must_use]
87    pub fn show_constraints(mut self, show: bool) -> Self {
88        self.show_constraints = show;
89        self
90    }
91
92    /// Set whether to show indexes.
93    #[must_use]
94    pub fn show_indexes(mut self, show: bool) -> Self {
95        self.show_indexes = show;
96        self
97    }
98
99    /// Set whether to show foreign keys.
100    #[must_use]
101    pub fn show_foreign_keys(mut self, show: bool) -> Self {
102        self.show_foreign_keys = show;
103        self
104    }
105
106    /// Set the theme for styled output.
107    #[must_use]
108    pub fn theme(mut self, theme: Theme) -> Self {
109        self.theme = Some(theme);
110        self
111    }
112
113    /// Use ASCII characters instead of Unicode.
114    #[must_use]
115    pub fn ascii(mut self) -> Self {
116        self.use_unicode = false;
117        self
118    }
119
120    /// Use Unicode box drawing characters.
121    #[must_use]
122    pub fn unicode(mut self) -> Self {
123        self.use_unicode = true;
124        self
125    }
126}
127
128/// Simplified table info for rendering (avoids dependency on sqlmodel-schema).
129#[derive(Debug, Clone)]
130pub struct TableData {
131    /// Table name
132    pub name: String,
133    /// Columns
134    pub columns: Vec<ColumnData>,
135    /// Primary key column names
136    pub primary_key: Vec<String>,
137    /// Foreign keys
138    pub foreign_keys: Vec<ForeignKeyData>,
139    /// Indexes
140    pub indexes: Vec<IndexData>,
141}
142
143/// Simplified column info for rendering.
144#[derive(Debug, Clone)]
145pub struct ColumnData {
146    /// Column name
147    pub name: String,
148    /// SQL type
149    pub sql_type: String,
150    /// Whether nullable
151    pub nullable: bool,
152    /// Default value
153    pub default: Option<String>,
154    /// Is primary key
155    pub primary_key: bool,
156    /// Auto increment
157    pub auto_increment: bool,
158}
159
160/// Simplified foreign key info for rendering.
161#[derive(Debug, Clone)]
162pub struct ForeignKeyData {
163    /// Constraint name
164    pub name: Option<String>,
165    /// Local column
166    pub column: String,
167    /// Foreign table
168    pub foreign_table: String,
169    /// Foreign column
170    pub foreign_column: String,
171    /// ON DELETE action
172    pub on_delete: Option<String>,
173    /// ON UPDATE action
174    pub on_update: Option<String>,
175}
176
177/// Simplified index info for rendering.
178#[derive(Debug, Clone)]
179pub struct IndexData {
180    /// Index name
181    pub name: String,
182    /// Columns
183    pub columns: Vec<String>,
184    /// Is unique
185    pub unique: bool,
186}
187
188/// Schema tree view for visualizing database structure.
189///
190/// Displays database tables and their columns as an ASCII/Unicode tree.
191#[derive(Debug, Clone)]
192pub struct SchemaTree {
193    /// Tables to display
194    tables: Vec<TableData>,
195    /// Configuration
196    config: SchemaTreeConfig,
197}
198
199impl SchemaTree {
200    /// Create a new schema tree from table data.
201    #[must_use]
202    pub fn new(tables: &[TableData]) -> Self {
203        Self {
204            tables: tables.to_vec(),
205            config: SchemaTreeConfig::default(),
206        }
207    }
208
209    /// Create an empty schema tree.
210    #[must_use]
211    pub fn empty() -> Self {
212        Self {
213            tables: Vec::new(),
214            config: SchemaTreeConfig::default(),
215        }
216    }
217
218    /// Add a table to the schema tree.
219    #[must_use]
220    pub fn add_table(mut self, table: TableData) -> Self {
221        self.tables.push(table);
222        self
223    }
224
225    /// Set the configuration.
226    #[must_use]
227    pub fn config(mut self, config: SchemaTreeConfig) -> Self {
228        self.config = config;
229        self
230    }
231
232    /// Set the theme for styled output.
233    #[must_use]
234    pub fn theme(mut self, theme: Theme) -> Self {
235        self.config.theme = Some(theme);
236        self
237    }
238
239    /// Use ASCII characters instead of Unicode.
240    #[must_use]
241    pub fn ascii(mut self) -> Self {
242        self.config.use_unicode = false;
243        self
244    }
245
246    /// Use Unicode box drawing characters.
247    #[must_use]
248    pub fn unicode(mut self) -> Self {
249        self.config.use_unicode = true;
250        self
251    }
252
253    /// Get tree drawing characters.
254    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    /// Render the schema as plain text.
263    #[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    /// Render a table in plain text.
282    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        // Table name with icon
293        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        // Calculate total children for proper connectors
307        #[allow(clippy::type_complexity)]
308        let mut children: Vec<(&str, Box<dyn Fn(&str, bool, &mut Vec<String>) + '_>)> = Vec::new();
309
310        // Columns section
311        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        // Indexes section
322        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        // Foreign keys section
333        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        // Render all sections
344        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(&section_prefix, true, lines);
357        }
358    }
359
360    /// Render columns in plain text.
361    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    /// Render indexes in plain text.
405    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    /// Render foreign keys in plain text.
429    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    /// Render the schema as styled text with ANSI colors.
461    #[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    /// Render a table with styling.
486    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        // Table name
504        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        // Sections
521        #[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(&section_prefix, true, lines, theme);
573        }
574    }
575
576    /// Render columns with styling.
577    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    /// Render indexes with styling.
633    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    /// Render foreign keys with styling.
667    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    /// Render as JSON-serializable structure.
705    #[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    /// Convert a table to JSON.
717    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}