1use crate::theme::Theme;
28use std::time::Duration;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum PlainFormat {
33 #[default]
35 Pipe,
36 Csv,
38 JsonLines,
40 JsonArray,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum ValueType {
47 Null,
49 Boolean,
51 Integer,
53 Float,
55 #[default]
57 String,
58 Date,
60 Time,
62 Timestamp,
64 Binary,
66 Json,
68 Uuid,
70}
71
72impl ValueType {
73 #[must_use]
75 pub fn infer(value: &str) -> Self {
76 let trimmed = value.trim();
77
78 if trimmed.eq_ignore_ascii_case("null") || trimmed.eq_ignore_ascii_case("<null>") {
80 return Self::Null;
81 }
82
83 if trimmed.eq_ignore_ascii_case("true") || trimmed.eq_ignore_ascii_case("false") {
85 return Self::Boolean;
86 }
87
88 if trimmed.starts_with("[BLOB:") || trimmed.starts_with("<binary:") {
90 return Self::Binary;
91 }
92
93 if (trimmed.starts_with('{') && trimmed.ends_with('}'))
95 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
96 {
97 return Self::Json;
98 }
99
100 if trimmed.len() == 36 && trimmed.chars().filter(|c| *c == '-').count() == 4 {
102 let parts: Vec<&str> = trimmed.split('-').collect();
103 if parts.len() == 5
104 && parts[0].len() == 8
105 && parts[1].len() == 4
106 && parts[2].len() == 4
107 && parts[3].len() == 4
108 && parts[4].len() == 12
109 && parts
110 .iter()
111 .all(|p| p.chars().all(|c| c.is_ascii_hexdigit()))
112 {
113 return Self::Uuid;
114 }
115 }
116
117 if trimmed.len() == 10 && trimmed.chars().filter(|c| *c == '-').count() == 2 {
119 if let Some(year) = trimmed.get(0..4) {
120 if year.parse::<u32>().is_ok() {
121 return Self::Date;
122 }
123 }
124 }
125
126 if trimmed.contains('T') && trimmed.len() >= 19 {
128 return Self::Timestamp;
129 }
130 if trimmed.len() >= 19 && trimmed.contains(' ') && trimmed.contains(':') {
131 return Self::Timestamp;
132 }
133
134 if trimmed.len() >= 8 && trimmed.contains(':') && !trimmed.contains('-') {
136 let parts: Vec<&str> = trimmed.split(':').collect();
137 if parts.len() >= 2
138 && parts
139 .iter()
140 .all(|p| p.parse::<u32>().is_ok() || p.contains('.'))
141 {
142 return Self::Time;
143 }
144 }
145
146 if trimmed.parse::<i64>().is_ok() {
148 return Self::Integer;
149 }
150
151 if trimmed.parse::<f64>().is_ok() {
153 return Self::Float;
154 }
155
156 Self::String
158 }
159
160 #[must_use]
162 pub fn color_code(&self, theme: &Theme) -> String {
163 match self {
164 Self::Null => theme.null_value.color_code(),
165 Self::Boolean => theme.bool_value.color_code(),
166 Self::Integer | Self::Float => theme.number_value.color_code(),
167 Self::String => theme.string_value.color_code(),
168 Self::Date | Self::Time | Self::Timestamp => theme.date_value.color_code(),
169 Self::Binary => theme.binary_value.color_code(),
170 Self::Json => theme.json_value.color_code(),
171 Self::Uuid => theme.uuid_value.color_code(),
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct Cell {
179 pub value: String,
181 pub value_type: ValueType,
183}
184
185impl Cell {
186 #[must_use]
188 pub fn new(value: impl Into<String>) -> Self {
189 let value = value.into();
190 let value_type = ValueType::infer(&value);
191 Self { value, value_type }
192 }
193
194 #[must_use]
196 pub fn with_type(value: impl Into<String>, value_type: ValueType) -> Self {
197 Self {
198 value: value.into(),
199 value_type,
200 }
201 }
202
203 #[must_use]
205 pub fn null() -> Self {
206 Self {
207 value: "NULL".to_string(),
208 value_type: ValueType::Null,
209 }
210 }
211}
212
213#[derive(Debug, Clone)]
218pub struct QueryResultTable {
219 title: Option<String>,
221 columns: Vec<String>,
223 rows: Vec<Vec<Cell>>,
225 timing_ms: Option<f64>,
227 max_width: Option<usize>,
229 max_rows: Option<usize>,
231 show_row_numbers: bool,
233 theme: Option<Theme>,
235 plain_format: PlainFormat,
237}
238
239pub type QueryResults = QueryResultTable;
243
244impl QueryResultTable {
245 #[must_use]
247 pub fn new() -> Self {
248 Self {
249 title: None,
250 columns: Vec::new(),
251 rows: Vec::new(),
252 timing_ms: None,
253 max_width: None,
254 max_rows: None,
255 show_row_numbers: false,
256 theme: None,
257 plain_format: PlainFormat::Pipe,
258 }
259 }
260
261 #[must_use]
278 pub fn from_data(columns: Vec<String>, rows: Vec<Vec<String>>) -> Self {
279 let mut table = Self::new();
280 table.columns = columns;
281 table.rows = rows
282 .into_iter()
283 .map(|row| row.into_iter().map(Cell::new).collect())
284 .collect();
285 table
286 }
287
288 #[must_use]
290 pub fn title(mut self, title: impl Into<String>) -> Self {
291 self.title = Some(title.into());
292 self
293 }
294
295 #[must_use]
297 pub fn columns(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
298 self.columns = columns.into_iter().map(Into::into).collect();
299 self
300 }
301
302 #[must_use]
304 pub fn row(mut self, values: impl IntoIterator<Item = impl Into<String>>) -> Self {
305 let cells: Vec<Cell> = values.into_iter().map(|v| Cell::new(v)).collect();
306 self.rows.push(cells);
307 self
308 }
309
310 #[must_use]
312 pub fn row_cells(mut self, cells: Vec<Cell>) -> Self {
313 self.rows.push(cells);
314 self
315 }
316
317 #[must_use]
319 pub fn rows(
320 mut self,
321 rows: impl IntoIterator<Item = impl IntoIterator<Item = impl Into<String>>>,
322 ) -> Self {
323 for row in rows {
324 let cells: Vec<Cell> = row.into_iter().map(|v| Cell::new(v)).collect();
325 self.rows.push(cells);
326 }
327 self
328 }
329
330 #[must_use]
332 pub fn timing_ms(mut self, ms: f64) -> Self {
333 self.timing_ms = Some(ms);
334 self
335 }
336
337 #[must_use]
339 pub fn timing(mut self, duration: Duration) -> Self {
340 self.timing_ms = Some(duration.as_secs_f64() * 1000.0);
341 self
342 }
343
344 #[must_use]
346 pub fn max_width(mut self, width: usize) -> Self {
347 self.max_width = Some(width);
348 self
349 }
350
351 #[must_use]
353 pub fn max_rows(mut self, max: usize) -> Self {
354 self.max_rows = Some(max);
355 self
356 }
357
358 #[must_use]
360 pub fn with_row_numbers(mut self) -> Self {
361 self.show_row_numbers = true;
362 self
363 }
364
365 #[must_use]
367 pub fn theme(mut self, theme: Theme) -> Self {
368 self.theme = Some(theme);
369 self
370 }
371
372 #[must_use]
374 pub fn plain_format(mut self, format: PlainFormat) -> Self {
375 self.plain_format = format;
376 self
377 }
378
379 #[must_use]
381 pub fn row_count(&self) -> usize {
382 self.rows.len()
383 }
384
385 #[must_use]
387 pub fn column_count(&self) -> usize {
388 self.columns.len()
389 }
390
391 fn calculate_column_widths(&self) -> Vec<usize> {
393 let mut widths: Vec<usize> = self.columns.iter().map(|c| c.chars().count()).collect();
394
395 if self.show_row_numbers {
397 let row_num_width = self.rows.len().to_string().len().max(1);
398 widths.insert(0, row_num_width);
399 }
400
401 for row in &self.rows {
403 for (i, cell) in row.iter().enumerate() {
404 let col_idx = if self.show_row_numbers { i + 1 } else { i };
405 if col_idx < widths.len() {
406 widths[col_idx] = widths[col_idx].max(cell.value.chars().count());
407 }
408 }
409 }
410
411 if let Some(max_width) = self.max_width {
413 let total_padding = (widths.len() * 3) + 1; let available = max_width.saturating_sub(total_padding);
415 let per_col_max = available / widths.len().max(1);
416
417 for w in &mut widths {
418 *w = (*w).min(per_col_max.max(3)); }
420 }
421
422 widths
423 }
424
425 fn truncate_value(value: &str, width: usize) -> String {
427 if value.chars().count() <= width {
428 value.to_string()
429 } else if width <= 3 {
430 value.chars().take(width).collect()
431 } else {
432 let truncated: String = value.chars().take(width - 3).collect();
433 format!("{truncated}...")
434 }
435 }
436
437 #[must_use]
439 pub fn render_plain(&self) -> String {
440 self.render_plain_format(self.plain_format)
441 }
442
443 #[must_use]
445 pub fn render_plain_format(&self, format: PlainFormat) -> String {
446 match format {
447 PlainFormat::Pipe => self.render_pipe(),
448 PlainFormat::Csv => self.render_csv(),
449 PlainFormat::JsonLines => self.render_json_lines(),
450 PlainFormat::JsonArray => self.render_json_array(),
451 }
452 }
453
454 fn render_pipe(&self) -> String {
456 let mut lines = Vec::new();
457
458 if let Some(ms) = self.timing_ms {
460 lines.push(format!("# {} rows in {:.2}ms", self.rows.len(), ms));
461 }
462
463 let mut header = self.columns.join("|");
465 if self.show_row_numbers {
466 header = format!("#|{header}");
467 }
468 lines.push(header);
469
470 let display_rows = self.max_rows.unwrap_or(self.rows.len());
472 let truncated = self.rows.len() > display_rows;
473
474 for (idx, row) in self.rows.iter().take(display_rows).enumerate() {
476 let values: Vec<&str> = row.iter().map(|c| c.value.as_str()).collect();
477 let mut line = values.join("|");
478 if self.show_row_numbers {
479 line = format!("{}|{line}", idx + 1);
480 }
481 lines.push(line);
482 }
483
484 if truncated {
486 lines.push(format!(
487 "... and {} more rows",
488 self.rows.len() - display_rows
489 ));
490 }
491
492 lines.join("\n")
493 }
494
495 fn render_csv(&self) -> String {
497 let mut lines = Vec::new();
498
499 let header: Vec<String> = self.columns.iter().map(|c| Self::csv_escape(c)).collect();
501 lines.push(header.join(","));
502
503 let display_rows = self.max_rows.unwrap_or(self.rows.len());
505
506 for row in self.rows.iter().take(display_rows) {
508 let values: Vec<String> = row.iter().map(|c| Self::csv_escape(&c.value)).collect();
509 lines.push(values.join(","));
510 }
511
512 lines.join("\n")
513 }
514
515 fn csv_escape(value: &str) -> String {
517 if value.contains(',') || value.contains('"') || value.contains('\n') {
518 let escaped = value.replace('"', "\"\"");
519 format!("\"{escaped}\"")
520 } else {
521 value.to_string()
522 }
523 }
524
525 fn render_json_lines(&self) -> String {
527 let display_rows = self.max_rows.unwrap_or(self.rows.len());
528
529 self.rows
530 .iter()
531 .take(display_rows)
532 .map(|row| {
533 let obj: serde_json::Map<String, serde_json::Value> = self
534 .columns
535 .iter()
536 .zip(row.iter())
537 .map(|(col, cell)| {
538 let value = match cell.value_type {
539 ValueType::Null => serde_json::Value::Null,
540 ValueType::Boolean => {
541 serde_json::Value::Bool(cell.value.eq_ignore_ascii_case("true"))
542 }
543 ValueType::Integer => {
544 if let Ok(n) = cell.value.parse::<i64>() {
545 serde_json::Value::Number(n.into())
546 } else {
547 serde_json::Value::String(cell.value.clone())
548 }
549 }
550 ValueType::Float => {
551 if let Ok(n) = cell.value.parse::<f64>() {
552 serde_json::Number::from_f64(n).map_or_else(
553 || serde_json::Value::String(cell.value.clone()),
554 serde_json::Value::Number,
555 )
556 } else {
557 serde_json::Value::String(cell.value.clone())
558 }
559 }
560 _ => serde_json::Value::String(cell.value.clone()),
561 };
562 (col.clone(), value)
563 })
564 .collect();
565 serde_json::to_string(&obj).unwrap_or_else(|_| "{}".to_string())
566 })
567 .collect::<Vec<_>>()
568 .join("\n")
569 }
570
571 fn render_json_array(&self) -> String {
573 let display_rows = self.max_rows.unwrap_or(self.rows.len());
574
575 let array: Vec<serde_json::Map<String, serde_json::Value>> = self
576 .rows
577 .iter()
578 .take(display_rows)
579 .map(|row| {
580 self.columns
581 .iter()
582 .zip(row.iter())
583 .map(|(col, cell)| {
584 let value = match cell.value_type {
585 ValueType::Null => serde_json::Value::Null,
586 ValueType::Boolean => {
587 serde_json::Value::Bool(cell.value.eq_ignore_ascii_case("true"))
588 }
589 ValueType::Integer => {
590 if let Ok(n) = cell.value.parse::<i64>() {
591 serde_json::Value::Number(n.into())
592 } else {
593 serde_json::Value::String(cell.value.clone())
594 }
595 }
596 ValueType::Float => {
597 if let Ok(n) = cell.value.parse::<f64>() {
598 serde_json::Number::from_f64(n).map_or_else(
599 || serde_json::Value::String(cell.value.clone()),
600 serde_json::Value::Number,
601 )
602 } else {
603 serde_json::Value::String(cell.value.clone())
604 }
605 }
606 _ => serde_json::Value::String(cell.value.clone()),
607 };
608 (col.clone(), value)
609 })
610 .collect()
611 })
612 .collect();
613
614 serde_json::to_string_pretty(&array).unwrap_or_else(|_| "[]".to_string())
615 }
616
617 #[must_use]
619 pub fn render_styled(&self) -> String {
620 let theme = self.theme.clone().unwrap_or_default();
621 let widths = self.calculate_column_widths();
622
623 let border_color = theme.border.color_code();
624 let header_color = theme.header.color_code();
625 let dim = theme.dim.color_code();
626 let reset = "\x1b[0m";
627
628 let mut lines = Vec::new();
629
630 let total_width: usize = widths.iter().sum::<usize>() + (widths.len() * 3) + 1;
632
633 if let Some(ref title) = self.title {
635 let timing_str = self.timing_ms.map_or(String::new(), |ms| {
636 format!(" • {} rows in {:.2}ms", self.rows.len(), ms)
637 });
638 let full_title = format!(" {title}{timing_str} ");
639 let title_len = full_title.chars().count();
640 let left_pad = (total_width.saturating_sub(2).saturating_sub(title_len)) / 2;
641 let right_pad = total_width
642 .saturating_sub(2)
643 .saturating_sub(title_len)
644 .saturating_sub(left_pad);
645
646 lines.push(format!(
647 "{border_color}╭{}{}{}╮{reset}",
648 "─".repeat(left_pad),
649 full_title,
650 "─".repeat(right_pad)
651 ));
652 } else if let Some(ms) = self.timing_ms {
653 let timing_str = format!(" {} rows in {:.2}ms ", self.rows.len(), ms);
654 let timing_len = timing_str.chars().count();
655 let left_pad = (total_width.saturating_sub(2).saturating_sub(timing_len)) / 2;
656 let right_pad = total_width
657 .saturating_sub(2)
658 .saturating_sub(timing_len)
659 .saturating_sub(left_pad);
660
661 lines.push(format!(
662 "{border_color}╭{}{}{}╮{reset}",
663 "─".repeat(left_pad),
664 timing_str,
665 "─".repeat(right_pad)
666 ));
667 } else {
668 lines.push(format!(
669 "{border_color}╭{}╮{reset}",
670 "─".repeat(total_width - 2)
671 ));
672 }
673
674 let mut header_cells = Vec::new();
676 if self.show_row_numbers {
677 header_cells.push(format!("{dim}{:>width$}{reset}", "#", width = widths[0]));
678 }
679 for (i, col) in self.columns.iter().enumerate() {
680 let col_idx = if self.show_row_numbers { i + 1 } else { i };
681 let width = widths.get(col_idx).copied().unwrap_or(10);
682 let truncated = Self::truncate_value(col, width);
683 header_cells.push(format!(
684 "{header_color}{:width$}{reset}",
685 truncated,
686 width = width
687 ));
688 }
689 lines.push(format!(
690 "{border_color}│{reset} {} {border_color}│{reset}",
691 header_cells.join(&format!(" {border_color}│{reset} "))
692 ));
693
694 let separators: Vec<String> = widths.iter().map(|w| "─".repeat(*w)).collect();
696 lines.push(format!(
697 "{border_color}├─{}─┤{reset}",
698 separators.join("─┼─")
699 ));
700
701 let display_rows = self.max_rows.unwrap_or(self.rows.len());
703 let truncated = self.rows.len() > display_rows;
704
705 for (idx, row) in self.rows.iter().take(display_rows).enumerate() {
707 let mut cells = Vec::new();
708
709 if self.show_row_numbers {
710 let row_num_width = widths[0];
711 cells.push(format!(
712 "{dim}{:>width$}{reset}",
713 idx + 1,
714 width = row_num_width
715 ));
716 }
717
718 for (i, cell) in row.iter().enumerate() {
719 let col_idx = if self.show_row_numbers { i + 1 } else { i };
720 let width = widths.get(col_idx).copied().unwrap_or(10);
721 let truncated_val = Self::truncate_value(&cell.value, width);
722 let color = cell.value_type.color_code(&theme);
723
724 let formatted = match cell.value_type {
726 ValueType::Integer | ValueType::Float => {
727 format!("{color}{:>width$}{reset}", truncated_val, width = width)
728 }
729 ValueType::Null => {
730 format!(
731 "{color}\x1b[3m{:^width$}\x1b[23m{reset}",
732 truncated_val,
733 width = width
734 )
735 }
736 _ => {
737 format!("{color}{:width$}{reset}", truncated_val, width = width)
738 }
739 };
740 cells.push(formatted);
741 }
742
743 lines.push(format!(
744 "{border_color}│{reset} {} {border_color}│{reset}",
745 cells.join(&format!(" {border_color}│{reset} "))
746 ));
747 }
748
749 if truncated {
751 let more_text = format!("... and {} more rows", self.rows.len() - display_rows);
752 let padding = total_width
753 .saturating_sub(4)
754 .saturating_sub(more_text.len());
755 lines.push(format!(
756 "{border_color}│{reset} {dim}{more_text}{:padding$}{reset} {border_color}│{reset}",
757 "",
758 padding = padding
759 ));
760 }
761
762 lines.push(format!(
764 "{border_color}╰{}╯{reset}",
765 "─".repeat(total_width - 2)
766 ));
767
768 lines.join("\n")
769 }
770
771 #[must_use]
773 pub fn to_json(&self) -> serde_json::Value {
774 let rows: Vec<serde_json::Value> = self
775 .rows
776 .iter()
777 .map(|row| {
778 let obj: serde_json::Map<String, serde_json::Value> = self
779 .columns
780 .iter()
781 .zip(row.iter())
782 .map(|(col, cell)| {
783 let value = match cell.value_type {
784 ValueType::Null => serde_json::Value::Null,
785 ValueType::Boolean => {
786 serde_json::Value::Bool(cell.value.eq_ignore_ascii_case("true"))
787 }
788 ValueType::Integer => {
789 if let Ok(n) = cell.value.parse::<i64>() {
790 serde_json::Value::Number(n.into())
791 } else {
792 serde_json::Value::String(cell.value.clone())
793 }
794 }
795 ValueType::Float => {
796 if let Ok(n) = cell.value.parse::<f64>() {
797 serde_json::Number::from_f64(n).map_or_else(
798 || serde_json::Value::String(cell.value.clone()),
799 serde_json::Value::Number,
800 )
801 } else {
802 serde_json::Value::String(cell.value.clone())
803 }
804 }
805 _ => serde_json::Value::String(cell.value.clone()),
806 };
807 (col.clone(), value)
808 })
809 .collect();
810 serde_json::Value::Object(obj)
811 })
812 .collect();
813
814 serde_json::json!({
815 "columns": self.columns,
816 "rows": rows,
817 "row_count": self.rows.len(),
818 "timing_ms": self.timing_ms,
819 })
820 }
821}
822
823impl Default for QueryResultTable {
824 fn default() -> Self {
825 Self::new()
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 #[test]
834 fn test_query_result_table_new() {
835 let table = QueryResultTable::new();
836 assert_eq!(table.row_count(), 0);
837 assert_eq!(table.column_count(), 0);
838 }
839
840 #[test]
841 fn test_query_result_table_basic() {
842 let table = QueryResultTable::new()
843 .columns(vec!["id", "name"])
844 .row(vec!["1", "Alice"])
845 .row(vec!["2", "Bob"]);
846
847 assert_eq!(table.row_count(), 2);
848 assert_eq!(table.column_count(), 2);
849 }
850
851 #[test]
852 fn test_value_type_inference_null() {
853 assert_eq!(ValueType::infer("null"), ValueType::Null);
854 assert_eq!(ValueType::infer("NULL"), ValueType::Null);
855 assert_eq!(ValueType::infer("<null>"), ValueType::Null);
856 }
857
858 #[test]
859 fn test_value_type_inference_boolean() {
860 assert_eq!(ValueType::infer("true"), ValueType::Boolean);
861 assert_eq!(ValueType::infer("false"), ValueType::Boolean);
862 assert_eq!(ValueType::infer("TRUE"), ValueType::Boolean);
863 }
864
865 #[test]
866 fn test_value_type_inference_integer() {
867 assert_eq!(ValueType::infer("42"), ValueType::Integer);
868 assert_eq!(ValueType::infer("-123"), ValueType::Integer);
869 assert_eq!(ValueType::infer("0"), ValueType::Integer);
870 }
871
872 #[test]
873 fn test_value_type_inference_float() {
874 assert_eq!(ValueType::infer("3.14"), ValueType::Float);
875 assert_eq!(ValueType::infer("-2.5"), ValueType::Float);
876 assert_eq!(ValueType::infer("1.0e10"), ValueType::Float);
877 }
878
879 #[test]
880 fn test_value_type_inference_date() {
881 assert_eq!(ValueType::infer("2024-01-15"), ValueType::Date);
882 }
883
884 #[test]
885 fn test_value_type_inference_timestamp() {
886 assert_eq!(
887 ValueType::infer("2024-01-15T10:30:00"),
888 ValueType::Timestamp
889 );
890 assert_eq!(
891 ValueType::infer("2024-01-15 10:30:00"),
892 ValueType::Timestamp
893 );
894 }
895
896 #[test]
897 fn test_value_type_inference_time() {
898 assert_eq!(ValueType::infer("10:30:00"), ValueType::Time);
899 assert_eq!(ValueType::infer("10:30:00.123"), ValueType::Time);
900 }
901
902 #[test]
903 fn test_value_type_inference_uuid() {
904 assert_eq!(
905 ValueType::infer("550e8400-e29b-41d4-a716-446655440000"),
906 ValueType::Uuid
907 );
908 }
909
910 #[test]
911 fn test_value_type_inference_json() {
912 assert_eq!(ValueType::infer("{\"key\": \"value\"}"), ValueType::Json);
913 assert_eq!(ValueType::infer("[1, 2, 3]"), ValueType::Json);
914 }
915
916 #[test]
917 fn test_value_type_inference_binary() {
918 assert_eq!(ValueType::infer("[BLOB: 1024 bytes]"), ValueType::Binary);
919 }
920
921 #[test]
922 fn test_value_type_inference_string() {
923 assert_eq!(ValueType::infer("hello"), ValueType::String);
924 assert_eq!(ValueType::infer("alice@example.com"), ValueType::String);
925 }
926
927 #[test]
928 fn test_render_pipe_basic() {
929 let table = QueryResultTable::new()
930 .columns(vec!["id", "name"])
931 .row(vec!["1", "Alice"])
932 .row(vec!["2", "Bob"]);
933
934 let output = table.render_plain();
935 assert!(output.contains("id|name"));
936 assert!(output.contains("1|Alice"));
937 assert!(output.contains("2|Bob"));
938 }
939
940 #[test]
941 fn test_render_pipe_with_timing() {
942 let table = QueryResultTable::new()
943 .columns(vec!["id"])
944 .row(vec!["1"])
945 .timing_ms(12.34);
946
947 let output = table.render_plain();
948 assert!(output.contains("# 1 rows in 12.34ms"));
949 }
950
951 #[test]
952 fn test_render_pipe_with_row_numbers() {
953 let table = QueryResultTable::new()
954 .columns(vec!["name"])
955 .row(vec!["Alice"])
956 .row(vec!["Bob"])
957 .with_row_numbers();
958
959 let output = table.render_plain();
960 assert!(output.contains("#|name"));
961 assert!(output.contains("1|Alice"));
962 assert!(output.contains("2|Bob"));
963 }
964
965 #[test]
966 fn test_render_csv_basic() {
967 let table = QueryResultTable::new()
968 .columns(vec!["id", "name"])
969 .row(vec!["1", "Alice"]);
970
971 let output = table.render_plain_format(PlainFormat::Csv);
972 assert!(output.contains("id,name"));
973 assert!(output.contains("1,Alice"));
974 }
975
976 #[test]
977 fn test_render_csv_escaping() {
978 let table = QueryResultTable::new()
979 .columns(vec!["text"])
980 .row(vec!["hello, world"]);
981
982 let output = table.render_plain_format(PlainFormat::Csv);
983 assert!(output.contains("\"hello, world\""));
984 }
985
986 #[test]
987 fn test_render_json_lines() {
988 let table = QueryResultTable::new()
989 .columns(vec!["id", "name"])
990 .row(vec!["1", "Alice"]);
991
992 let output = table.render_plain_format(PlainFormat::JsonLines);
993 assert!(output.contains("\"id\":1"));
994 assert!(output.contains("\"name\":\"Alice\""));
995 }
996
997 #[test]
998 fn test_render_json_array() {
999 let table = QueryResultTable::new()
1000 .columns(vec!["id"])
1001 .row(vec!["1"])
1002 .row(vec!["2"]);
1003
1004 let output = table.render_plain_format(PlainFormat::JsonArray);
1005 assert!(output.starts_with('['));
1006 assert!(output.ends_with(']'));
1007 }
1008
1009 #[test]
1010 fn test_max_rows_truncation() {
1011 let table = QueryResultTable::new()
1012 .columns(vec!["id"])
1013 .row(vec!["1"])
1014 .row(vec!["2"])
1015 .row(vec!["3"])
1016 .row(vec!["4"])
1017 .row(vec!["5"])
1018 .max_rows(3);
1019
1020 let output = table.render_plain();
1021 assert!(output.contains("... and 2 more rows"));
1022 }
1023
1024 #[test]
1025 fn test_cell_new() {
1026 let cell = Cell::new("42");
1027 assert_eq!(cell.value, "42");
1028 assert_eq!(cell.value_type, ValueType::Integer);
1029 }
1030
1031 #[test]
1032 fn test_cell_with_type() {
1033 let cell = Cell::with_type("hello", ValueType::String);
1034 assert_eq!(cell.value, "hello");
1035 assert_eq!(cell.value_type, ValueType::String);
1036 }
1037
1038 #[test]
1039 fn test_cell_null() {
1040 let cell = Cell::null();
1041 assert_eq!(cell.value, "NULL");
1042 assert_eq!(cell.value_type, ValueType::Null);
1043 }
1044
1045 #[test]
1046 fn test_truncate_value_short() {
1047 assert_eq!(QueryResultTable::truncate_value("abc", 10), "abc");
1048 }
1049
1050 #[test]
1051 fn test_truncate_value_long() {
1052 assert_eq!(
1053 QueryResultTable::truncate_value("hello world", 8),
1054 "hello..."
1055 );
1056 }
1057
1058 #[test]
1059 fn test_truncate_value_exact() {
1060 assert_eq!(QueryResultTable::truncate_value("hello", 5), "hello");
1061 }
1062
1063 #[test]
1064 fn test_to_json() {
1065 let table = QueryResultTable::new()
1066 .columns(vec!["id", "name"])
1067 .row(vec!["1", "Alice"])
1068 .timing_ms(10.0);
1069
1070 let json = table.to_json();
1071 assert_eq!(json["row_count"], 1);
1072 assert_eq!(json["timing_ms"], 10.0);
1073 assert!(json["columns"].is_array());
1074 assert!(json["rows"].is_array());
1075 }
1076
1077 #[test]
1078 fn test_render_styled_contains_box() {
1079 let table = QueryResultTable::new().columns(vec!["id"]).row(vec!["1"]);
1080
1081 let styled = table.render_styled();
1082 assert!(styled.contains("╭"));
1083 assert!(styled.contains("╯"));
1084 assert!(styled.contains("│"));
1085 }
1086
1087 #[test]
1088 fn test_render_styled_with_title() {
1089 let table = QueryResultTable::new()
1090 .title("Test Results")
1091 .columns(vec!["id"])
1092 .row(vec!["1"]);
1093
1094 let styled = table.render_styled();
1095 assert!(styled.contains("Test Results"));
1096 }
1097
1098 #[test]
1099 fn test_builder_chain() {
1100 let table = QueryResultTable::new()
1101 .title("My Table")
1102 .columns(vec!["a", "b"])
1103 .row(vec!["1", "2"])
1104 .timing_ms(5.0)
1105 .max_width(80)
1106 .max_rows(100)
1107 .with_row_numbers()
1108 .theme(Theme::dark())
1109 .plain_format(PlainFormat::Csv);
1110
1111 assert_eq!(table.row_count(), 1);
1112 assert_eq!(table.column_count(), 2);
1113 }
1114
1115 #[test]
1116 fn test_null_values_in_json() {
1117 let table = QueryResultTable::new()
1118 .columns(vec!["value"])
1119 .row(vec!["null"]);
1120
1121 let json = table.to_json();
1122 let rows = json["rows"].as_array().unwrap();
1123 assert!(rows[0]["value"].is_null());
1124 }
1125
1126 #[test]
1127 fn test_boolean_values_in_json() {
1128 let table = QueryResultTable::new()
1129 .columns(vec!["flag"])
1130 .row(vec!["true"]);
1131
1132 let json = table.to_json();
1133 let rows = json["rows"].as_array().unwrap();
1134 assert_eq!(rows[0]["flag"], true);
1135 }
1136
1137 #[test]
1138 fn test_integer_values_in_json() {
1139 let table = QueryResultTable::new()
1140 .columns(vec!["count"])
1141 .row(vec!["42"]);
1142
1143 let json = table.to_json();
1144 let rows = json["rows"].as_array().unwrap();
1145 assert_eq!(rows[0]["count"], 42);
1146 }
1147}