1use crate::theme::Theme;
45
46pub use super::schema_tree::{ColumnData, ForeignKeyData, IndexData};
48
49#[derive(Debug, Clone, Default)]
51pub struct TableStats {
52 pub row_count: Option<u64>,
54 pub size_bytes: Option<u64>,
56 pub index_size_bytes: Option<u64>,
58 pub last_analyzed: Option<String>,
60 pub last_modified: Option<String>,
62}
63
64impl TableStats {
65 #[must_use]
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 #[must_use]
73 pub fn row_count(mut self, count: u64) -> Self {
74 self.row_count = Some(count);
75 self
76 }
77
78 #[must_use]
80 pub fn size_bytes(mut self, bytes: u64) -> Self {
81 self.size_bytes = Some(bytes);
82 self
83 }
84
85 #[must_use]
87 pub fn index_size_bytes(mut self, bytes: u64) -> Self {
88 self.index_size_bytes = Some(bytes);
89 self
90 }
91
92 #[must_use]
94 pub fn last_analyzed<S: Into<String>>(mut self, ts: S) -> Self {
95 self.last_analyzed = Some(ts.into());
96 self
97 }
98
99 #[must_use]
101 pub fn last_modified<S: Into<String>>(mut self, ts: S) -> Self {
102 self.last_modified = Some(ts.into());
103 self
104 }
105
106 #[must_use]
108 pub fn is_empty(&self) -> bool {
109 self.row_count.is_none()
110 && self.size_bytes.is_none()
111 && self.index_size_bytes.is_none()
112 && self.last_analyzed.is_none()
113 && self.last_modified.is_none()
114 }
115}
116
117#[derive(Debug, Clone)]
122pub struct TableInfo {
123 name: String,
125 schema: Option<String>,
127 columns: Vec<ColumnData>,
129 primary_key: Vec<String>,
131 indexes: Vec<IndexData>,
133 foreign_keys: Vec<ForeignKeyData>,
135 stats: Option<TableStats>,
137 theme: Theme,
139 width: Option<usize>,
141 show_types: bool,
143 show_constraints: bool,
145}
146
147impl TableInfo {
148 #[must_use]
150 pub fn new<S: Into<String>>(name: S, columns: Vec<ColumnData>) -> Self {
151 Self {
152 name: name.into(),
153 schema: None,
154 columns,
155 primary_key: Vec::new(),
156 indexes: Vec::new(),
157 foreign_keys: Vec::new(),
158 stats: None,
159 theme: Theme::default(),
160 width: None,
161 show_types: true,
162 show_constraints: true,
163 }
164 }
165
166 #[must_use]
168 pub fn empty<S: Into<String>>(name: S) -> Self {
169 Self::new(name, Vec::new())
170 }
171
172 #[must_use]
174 pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
175 self.schema = Some(schema.into());
176 self
177 }
178
179 #[must_use]
181 pub fn with_primary_key(mut self, columns: Vec<String>) -> Self {
182 self.primary_key = columns;
183 self
184 }
185
186 #[must_use]
188 pub fn add_index(mut self, index: IndexData) -> Self {
189 self.indexes.push(index);
190 self
191 }
192
193 #[must_use]
195 pub fn with_indexes(mut self, indexes: Vec<IndexData>) -> Self {
196 self.indexes = indexes;
197 self
198 }
199
200 #[must_use]
202 pub fn add_foreign_key(mut self, fk: ForeignKeyData) -> Self {
203 self.foreign_keys.push(fk);
204 self
205 }
206
207 #[must_use]
209 pub fn with_foreign_keys(mut self, fks: Vec<ForeignKeyData>) -> Self {
210 self.foreign_keys = fks;
211 self
212 }
213
214 #[must_use]
216 pub fn with_stats(mut self, stats: TableStats) -> Self {
217 self.stats = Some(stats);
218 self
219 }
220
221 #[must_use]
223 pub fn theme(mut self, theme: Theme) -> Self {
224 self.theme = theme;
225 self
226 }
227
228 #[must_use]
230 pub fn width(mut self, width: usize) -> Self {
231 self.width = Some(width);
232 self
233 }
234
235 #[must_use]
237 pub fn show_types(mut self, show: bool) -> Self {
238 self.show_types = show;
239 self
240 }
241
242 #[must_use]
244 pub fn show_constraints(mut self, show: bool) -> Self {
245 self.show_constraints = show;
246 self
247 }
248
249 #[must_use]
251 pub fn full_name(&self) -> String {
252 if let Some(ref schema) = self.schema {
253 format!("{}.{}", schema, self.name)
254 } else {
255 self.name.clone()
256 }
257 }
258
259 #[must_use]
261 pub fn render_plain(&self) -> String {
262 let mut lines = Vec::new();
263
264 let pk_info = if self.primary_key.is_empty() {
266 String::new()
267 } else {
268 format!(" (PK: {})", self.primary_key.join(", "))
269 };
270
271 lines.push(format!("TABLE: {}{}", self.full_name(), pk_info));
272 lines.push("=".repeat(lines[0].len().min(60)));
273
274 if let Some(ref stats) = self.stats {
276 if !stats.is_empty() {
277 let mut stats_parts = Vec::new();
278 if let Some(rows) = stats.row_count {
279 stats_parts.push(format!("Rows: {}", format_number(rows)));
280 }
281 if let Some(size) = stats.size_bytes {
282 stats_parts.push(format!("Size: {}", format_bytes(size)));
283 }
284 if let Some(idx_size) = stats.index_size_bytes {
285 stats_parts.push(format!("Index Size: {}", format_bytes(idx_size)));
286 }
287 if !stats_parts.is_empty() {
288 lines.push(stats_parts.join(" | "));
289 }
290 if let Some(ref analyzed) = stats.last_analyzed {
291 lines.push(format!("Last Analyzed: {}", analyzed));
292 }
293 if let Some(ref modified) = stats.last_modified {
294 lines.push(format!("Last Modified: {}", modified));
295 }
296 lines.push(String::new());
297 }
298 }
299
300 lines.push("COLUMNS:".to_string());
302 lines.push("-".repeat(40));
303
304 if self.columns.is_empty() {
305 lines.push(" (no columns)".to_string());
306 } else {
307 let max_name_len = self.columns.iter().map(|c| c.name.len()).max().unwrap_or(4);
309 let max_type_len = self
310 .columns
311 .iter()
312 .map(|c| c.sql_type.len())
313 .max()
314 .unwrap_or(4);
315
316 for col in &self.columns {
317 let mut parts = vec![format!(" {:<width$}", col.name, width = max_name_len)];
318
319 if self.show_types {
320 parts.push(format!("{:<width$}", col.sql_type, width = max_type_len));
321 }
322
323 if self.show_constraints {
324 let mut constraints: Vec<String> = Vec::new();
325 if col.primary_key {
326 constraints.push("PK".into());
327 }
328 if col.auto_increment {
329 constraints.push("AUTO".into());
330 }
331 if col.nullable {
332 constraints.push("NULL".into());
333 } else {
334 constraints.push("NOT NULL".into());
335 }
336 if let Some(ref default) = col.default {
337 constraints.push(format!("DEFAULT {}", default));
338 }
339 parts.push(format!("[{}]", constraints.join(", ")));
340 }
341
342 lines.push(parts.join(" "));
343 }
344 }
345
346 if !self.indexes.is_empty() {
348 lines.push(String::new());
349 lines.push("INDEXES:".to_string());
350 lines.push("-".repeat(40));
351
352 for idx in &self.indexes {
353 let unique_marker = if idx.unique { "UNIQUE " } else { "" };
354 lines.push(format!(
355 " {}{} ({})",
356 unique_marker,
357 idx.name,
358 idx.columns.join(", ")
359 ));
360 }
361 }
362
363 if !self.foreign_keys.is_empty() {
365 lines.push(String::new());
366 lines.push("FOREIGN KEYS:".to_string());
367 lines.push("-".repeat(40));
368
369 for fk in &self.foreign_keys {
370 let name = fk.name.as_deref().unwrap_or("(unnamed)");
371 let mut parts = vec![format!(
372 " {}: {} -> {}.{}",
373 name, fk.column, fk.foreign_table, fk.foreign_column
374 )];
375
376 if let Some(ref on_delete) = fk.on_delete {
377 parts.push(format!("ON DELETE {}", on_delete));
378 }
379 if let Some(ref on_update) = fk.on_update {
380 parts.push(format!("ON UPDATE {}", on_update));
381 }
382
383 lines.push(parts.join(" "));
384 }
385 }
386
387 lines.join("\n")
388 }
389
390 #[must_use]
392 pub fn render_styled(&self) -> String {
393 let width = self.width.unwrap_or(70);
394 let reset = "\x1b[0m";
395 let dim = self.theme.dim.color_code();
396 let header_color = self.theme.header.color_code();
397 let keyword_color = self.theme.sql_keyword.color_code();
398 let name_color = self.theme.sql_identifier.color_code();
399 let type_color = self.theme.sql_keyword.color_code();
400 let success_color = self.theme.success.color_code();
401 let info_color = self.theme.info.color_code();
402
403 let mut lines = Vec::new();
404
405 lines.push(format!("{dim}┌{}┐{reset}", "─".repeat(width - 2)));
407
408 let pk_info = if self.primary_key.is_empty() {
410 String::new()
411 } else {
412 format!(" {dim}(PK: {}){reset}", self.primary_key.join(", "))
413 };
414
415 let title = format!(
416 "{keyword_color}TABLE:{reset} {name_color}{}{reset}{pk_info}",
417 self.full_name()
418 );
419 let title_visible_len = self.full_name().len()
420 + 7
421 + if self.primary_key.is_empty() {
422 0
423 } else {
424 6 + self.primary_key.join(", ").len()
425 };
426 let padding = width.saturating_sub(title_visible_len + 4);
427 lines.push(format!(
428 "{dim}│{reset} {}{:padding$} {dim}│{reset}",
429 title,
430 "",
431 padding = padding
432 ));
433
434 lines.push(format!("{dim}├{}┤{reset}", "─".repeat(width - 2)));
436
437 if let Some(ref stats) = self.stats {
439 if !stats.is_empty() {
440 let mut stats_parts = Vec::new();
441 if let Some(rows) = stats.row_count {
442 stats_parts.push(format!("{info_color}Rows:{reset} {}", format_number(rows)));
443 }
444 if let Some(size) = stats.size_bytes {
445 stats_parts.push(format!("{info_color}Size:{reset} {}", format_bytes(size)));
446 }
447 if let Some(idx_size) = stats.index_size_bytes {
448 stats_parts.push(format!(
449 "{info_color}Idx:{reset} {}",
450 format_bytes(idx_size)
451 ));
452 }
453 if !stats_parts.is_empty() {
454 let stats_line = stats_parts.join(" {dim}│{reset} ");
455 lines.push(format!(
456 "{dim}│{reset} {:<width$} {dim}│{reset}",
457 stats_line,
458 width = width - 4
459 ));
460 }
461 if let Some(ref analyzed) = stats.last_analyzed {
462 lines.push(format!(
463 "{dim}│{reset} {info_color}Analyzed:{reset} {:<width$} {dim}│{reset}",
464 analyzed,
465 width = width - 14
466 ));
467 }
468 lines.push(format!("{dim}├{}┤{reset}", "─".repeat(width - 2)));
469 }
470 }
471
472 lines.push(format!(
474 "{dim}│{reset} {header_color}COLUMNS{reset}{:width$} {dim}│{reset}",
475 "",
476 width = width - 11
477 ));
478
479 if self.columns.is_empty() {
481 lines.push(format!(
482 "{dim}│{reset} {dim}(no columns){reset}{:width$} {dim}│{reset}",
483 "",
484 width = width - 17
485 ));
486 } else {
487 let max_name_len = self.columns.iter().map(|c| c.name.len()).max().unwrap_or(4);
488 let max_type_len = self
489 .columns
490 .iter()
491 .map(|c| c.sql_type.len())
492 .max()
493 .unwrap_or(4);
494
495 for col in &self.columns {
496 let mut content = format!(
497 " {name_color}{:<name_w$}{reset} {type_color}{:<type_w$}{reset}",
498 col.name,
499 col.sql_type,
500 name_w = max_name_len,
501 type_w = max_type_len
502 );
503
504 if self.show_constraints {
505 let mut constraints = Vec::new();
506 if col.primary_key {
507 constraints.push(format!("{success_color}PK{reset}"));
508 }
509 if col.auto_increment {
510 constraints.push(format!("{info_color}AUTO{reset}"));
511 }
512 if !col.nullable {
513 constraints.push(format!("{dim}NOT NULL{reset}"));
514 }
515 if let Some(ref default) = col.default {
516 constraints.push(format!("{dim}DEFAULT {}{reset}", default));
517 }
518 if !constraints.is_empty() {
519 content.push_str(&format!(" {}", constraints.join(" ")));
520 }
521 }
522
523 let visible_len = 2 + max_name_len + 2 + max_type_len + 20;
525 let padding = width.saturating_sub(visible_len + 4);
526 lines.push(format!(
527 "{dim}│{reset}{}{:padding$} {dim}│{reset}",
528 content,
529 "",
530 padding = padding
531 ));
532 }
533 }
534
535 if !self.indexes.is_empty() {
537 lines.push(format!("{dim}├{}┤{reset}", "─".repeat(width - 2)));
538 lines.push(format!(
539 "{dim}│{reset} {header_color}INDEXES{reset}{:width$} {dim}│{reset}",
540 "",
541 width = width - 11
542 ));
543
544 for idx in &self.indexes {
545 let unique_marker = if idx.unique {
546 format!("{keyword_color}UNIQUE {reset}")
547 } else {
548 String::new()
549 };
550 let content = format!(
551 " {}{name_color}{}{reset} {dim}({}){reset}",
552 unique_marker,
553 idx.name,
554 idx.columns.join(", ")
555 );
556 let visible_len = 2
557 + if idx.unique { 7 } else { 0 }
558 + idx.name.len()
559 + 3
560 + idx.columns.join(", ").len();
561 let padding = width.saturating_sub(visible_len + 4);
562 lines.push(format!(
563 "{dim}│{reset}{}{:padding$} {dim}│{reset}",
564 content,
565 "",
566 padding = padding
567 ));
568 }
569 }
570
571 if !self.foreign_keys.is_empty() {
573 lines.push(format!("{dim}├{}┤{reset}", "─".repeat(width - 2)));
574 lines.push(format!(
575 "{dim}│{reset} {header_color}FOREIGN KEYS{reset}{:width$} {dim}│{reset}",
576 "",
577 width = width - 16
578 ));
579
580 for fk in &self.foreign_keys {
581 let name = fk.name.as_deref().unwrap_or("(unnamed)");
582 let ref_color = self.theme.string_value.color_code();
583 let content = format!(
584 " {name_color}{}{reset}: {dim}{}{reset} → {ref_color}{}.{}{reset}",
585 name, fk.column, fk.foreign_table, fk.foreign_column
586 );
587
588 let mut actions = Vec::new();
589 if let Some(ref on_delete) = fk.on_delete {
590 actions.push(format!("{dim}DEL:{}{reset}", on_delete));
591 }
592 if let Some(ref on_update) = fk.on_update {
593 actions.push(format!("{dim}UPD:{}{reset}", on_update));
594 }
595
596 let full_content = if actions.is_empty() {
597 content
598 } else {
599 format!("{} {}", content, actions.join(" "))
600 };
601
602 let visible_len = 2
603 + name.len()
604 + 2
605 + fk.column.len()
606 + 3
607 + fk.foreign_table.len()
608 + 1
609 + fk.foreign_column.len();
610 let padding = width.saturating_sub(visible_len + 20);
611 lines.push(format!(
612 "{dim}│{reset}{}{:padding$} {dim}│{reset}",
613 full_content,
614 "",
615 padding = padding
616 ));
617 }
618 }
619
620 lines.push(format!("{dim}└{}┘{reset}", "─".repeat(width - 2)));
622
623 lines.join("\n")
624 }
625
626 #[must_use]
628 pub fn to_json(&self) -> serde_json::Value {
629 let columns: Vec<serde_json::Value> = self
630 .columns
631 .iter()
632 .map(|col| {
633 serde_json::json!({
634 "name": col.name,
635 "type": col.sql_type,
636 "nullable": col.nullable,
637 "default": col.default,
638 "primary_key": col.primary_key,
639 "auto_increment": col.auto_increment,
640 })
641 })
642 .collect();
643
644 let indexes: Vec<serde_json::Value> = self
645 .indexes
646 .iter()
647 .map(|idx| {
648 serde_json::json!({
649 "name": idx.name,
650 "columns": idx.columns,
651 "unique": idx.unique,
652 })
653 })
654 .collect();
655
656 let foreign_keys: Vec<serde_json::Value> = self
657 .foreign_keys
658 .iter()
659 .map(|fk| {
660 serde_json::json!({
661 "name": fk.name,
662 "column": fk.column,
663 "foreign_table": fk.foreign_table,
664 "foreign_column": fk.foreign_column,
665 "on_delete": fk.on_delete,
666 "on_update": fk.on_update,
667 })
668 })
669 .collect();
670
671 let mut result = serde_json::json!({
672 "table": {
673 "name": self.name,
674 "schema": self.schema,
675 "full_name": self.full_name(),
676 "columns": columns,
677 "primary_key": self.primary_key,
678 "indexes": indexes,
679 "foreign_keys": foreign_keys,
680 }
681 });
682
683 if let Some(ref stats) = self.stats {
684 result["table"]["stats"] = serde_json::json!({
685 "row_count": stats.row_count,
686 "size_bytes": stats.size_bytes,
687 "index_size_bytes": stats.index_size_bytes,
688 "last_analyzed": stats.last_analyzed,
689 "last_modified": stats.last_modified,
690 });
691 }
692
693 result
694 }
695}
696
697#[must_use]
708pub fn format_number(n: u64) -> String {
709 let s = n.to_string();
710 let bytes: Vec<char> = s.chars().collect();
711 let mut result = String::new();
712
713 for (i, &c) in bytes.iter().enumerate() {
714 if i > 0 && (bytes.len() - i) % 3 == 0 {
715 result.push(',');
716 }
717 result.push(c);
718 }
719
720 result
721}
722
723#[must_use]
735pub fn format_bytes(bytes: u64) -> String {
736 const KB: u64 = 1024;
737 const MB: u64 = KB * 1024;
738 const GB: u64 = MB * 1024;
739 const TB: u64 = GB * 1024;
740
741 if bytes >= TB {
742 format!("{:.1} TB", bytes as f64 / TB as f64)
743 } else if bytes >= GB {
744 format!("{:.1} GB", bytes as f64 / GB as f64)
745 } else if bytes >= MB {
746 format!("{:.1} MB", bytes as f64 / MB as f64)
747 } else if bytes >= KB {
748 format!("{:.1} KB", bytes as f64 / KB as f64)
749 } else {
750 format!("{} B", bytes)
751 }
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 fn sample_column(name: &str, sql_type: &str, primary_key: bool) -> ColumnData {
759 ColumnData {
760 name: name.to_string(),
761 sql_type: sql_type.to_string(),
762 nullable: !primary_key,
763 default: None,
764 primary_key,
765 auto_increment: primary_key,
766 }
767 }
768
769 fn sample_columns() -> Vec<ColumnData> {
770 vec![
771 sample_column("id", "BIGINT", true),
772 sample_column("name", "VARCHAR(255)", false),
773 sample_column("email", "VARCHAR(255)", false),
774 ColumnData {
775 name: "created_at".to_string(),
776 sql_type: "TIMESTAMP".to_string(),
777 nullable: false,
778 default: Some("CURRENT_TIMESTAMP".to_string()),
779 primary_key: false,
780 auto_increment: false,
781 },
782 ]
783 }
784
785 #[test]
786 fn test_table_info_creation() {
787 let info = TableInfo::new("users", sample_columns());
788 assert_eq!(info.name, "users");
789 assert_eq!(info.columns.len(), 4);
790 }
791
792 #[test]
793 fn test_table_info_empty() {
794 let info = TableInfo::empty("empty_table");
795 assert_eq!(info.name, "empty_table");
796 assert!(info.columns.is_empty());
797 }
798
799 #[test]
800 fn test_table_info_with_schema() {
801 let info = TableInfo::new("users", sample_columns()).schema("public");
802 assert_eq!(info.full_name(), "public.users");
803 }
804
805 #[test]
806 fn test_table_info_columns_display() {
807 let info = TableInfo::new("users", sample_columns());
808 let output = info.render_plain();
809 assert!(output.contains("id"));
810 assert!(output.contains("BIGINT"));
811 assert!(output.contains("name"));
812 assert!(output.contains("VARCHAR(255)"));
813 }
814
815 #[test]
816 fn test_table_info_primary_key() {
817 let info =
818 TableInfo::new("users", sample_columns()).with_primary_key(vec!["id".to_string()]);
819 let output = info.render_plain();
820 assert!(output.contains("(PK: id)"));
821 }
822
823 #[test]
824 fn test_table_info_indexes_section() {
825 let info = TableInfo::new("users", sample_columns()).add_index(IndexData {
826 name: "idx_email".to_string(),
827 columns: vec!["email".to_string()],
828 unique: true,
829 });
830 let output = info.render_plain();
831 assert!(output.contains("INDEXES:"));
832 assert!(output.contains("UNIQUE idx_email"));
833 assert!(output.contains("(email)"));
834 }
835
836 #[test]
837 fn test_table_info_foreign_keys() {
838 let info = TableInfo::new("posts", sample_columns()).add_foreign_key(ForeignKeyData {
839 name: Some("fk_user".to_string()),
840 column: "user_id".to_string(),
841 foreign_table: "users".to_string(),
842 foreign_column: "id".to_string(),
843 on_delete: Some("CASCADE".to_string()),
844 on_update: None,
845 });
846 let output = info.render_plain();
847 assert!(output.contains("FOREIGN KEYS:"));
848 assert!(output.contains("fk_user: user_id -> users.id"));
849 assert!(output.contains("ON DELETE CASCADE"));
850 }
851
852 #[test]
853 fn test_table_info_with_stats() {
854 let info = TableInfo::new("users", sample_columns()).with_stats(TableStats {
855 row_count: Some(10_000),
856 size_bytes: Some(2_500_000),
857 index_size_bytes: Some(500_000),
858 last_analyzed: Some("2026-01-22 10:30:00".to_string()),
859 last_modified: None,
860 });
861 let output = info.render_plain();
862 assert!(output.contains("Rows: 10,000"));
863 assert!(output.contains("Size: 2.4 MB"));
864 assert!(output.contains("Index Size: 488.3 KB"));
865 assert!(output.contains("Last Analyzed: 2026-01-22"));
866 }
867
868 #[test]
869 fn test_table_info_render_plain() {
870 let info = TableInfo::new("heroes", sample_columns());
871 let output = info.render_plain();
872 assert!(output.contains("TABLE: heroes"));
873 assert!(output.contains("COLUMNS:"));
874 }
875
876 #[test]
877 fn test_table_info_render_rich() {
878 let info = TableInfo::new("heroes", sample_columns()).width(80);
879 let styled = info.render_styled();
880 assert!(styled.contains('\x1b')); assert!(styled.contains("┌")); assert!(styled.contains("│"));
883 assert!(styled.contains("└"));
884 }
885
886 #[test]
887 fn test_table_info_width_constraint() {
888 let info = TableInfo::new("heroes", sample_columns()).width(60);
889 let styled = info.render_styled();
890 let lines: Vec<&str> = styled.lines().collect();
892 assert!(!lines.is_empty());
893 }
894
895 #[test]
896 fn test_table_info_empty_table() {
897 let info = TableInfo::empty("empty");
898 let output = info.render_plain();
899 assert!(output.contains("(no columns)"));
900 }
901
902 #[test]
903 fn test_table_info_to_json() {
904 let info = TableInfo::new("users", sample_columns())
905 .with_primary_key(vec!["id".to_string()])
906 .with_stats(TableStats::new().row_count(100));
907 let json = info.to_json();
908 assert_eq!(json["table"]["name"], "users");
909 assert!(json["table"]["columns"].is_array());
910 assert_eq!(json["table"]["primary_key"][0], "id");
911 assert_eq!(json["table"]["stats"]["row_count"], 100);
912 }
913
914 #[test]
915 fn test_format_number_thousands() {
916 assert_eq!(format_number(0), "0");
917 assert_eq!(format_number(999), "999");
918 assert_eq!(format_number(1000), "1,000");
919 assert_eq!(format_number(12345), "12,345");
920 assert_eq!(format_number(1_234_567), "1,234,567");
921 assert_eq!(format_number(1_234_567_890), "1,234,567,890");
922 }
923
924 #[test]
925 fn test_format_bytes_units() {
926 assert_eq!(format_bytes(0), "0 B");
927 assert_eq!(format_bytes(512), "512 B");
928 assert_eq!(format_bytes(1024), "1.0 KB");
929 assert_eq!(format_bytes(1536), "1.5 KB");
930 assert_eq!(format_bytes(1_048_576), "1.0 MB");
931 assert_eq!(format_bytes(1_572_864), "1.5 MB");
932 assert_eq!(format_bytes(1_073_741_824), "1.0 GB");
933 assert_eq!(format_bytes(1_099_511_627_776), "1.0 TB");
934 }
935
936 #[test]
937 fn test_table_stats_builder() {
938 let stats = TableStats::new()
939 .row_count(1000)
940 .size_bytes(50000)
941 .last_analyzed("2026-01-22");
942 assert_eq!(stats.row_count, Some(1000));
943 assert_eq!(stats.size_bytes, Some(50000));
944 assert_eq!(stats.last_analyzed, Some("2026-01-22".to_string()));
945 }
946
947 #[test]
948 fn test_table_stats_is_empty() {
949 let empty = TableStats::new();
950 assert!(empty.is_empty());
951
952 let with_data = TableStats::new().row_count(100);
953 assert!(!with_data.is_empty());
954 }
955
956 #[test]
957 fn test_table_info_builder_pattern() {
958 let info = TableInfo::new("test", vec![])
959 .schema("myschema")
960 .with_primary_key(vec!["id".to_string()])
961 .with_indexes(vec![IndexData {
962 name: "idx1".to_string(),
963 columns: vec!["col".to_string()],
964 unique: false,
965 }])
966 .with_foreign_keys(vec![])
967 .theme(Theme::light())
968 .width(100)
969 .show_types(false)
970 .show_constraints(false);
971
972 assert_eq!(info.schema, Some("myschema".to_string()));
973 assert_eq!(info.width, Some(100));
974 assert!(!info.show_types);
975 assert!(!info.show_constraints);
976 }
977}