Skip to main content

sqlmodel_console/renderables/
query_results.rs

1//! Query result table renderable for beautiful result display.
2//!
3//! Provides a table specifically designed for displaying query results with
4//! rich formatting in styled mode and multiple plain text formats for agent
5//! compatibility.
6//!
7//! # Example
8//!
9//! ```rust
10//! use sqlmodel_console::renderables::{QueryResultTable, PlainFormat};
11//!
12//! let table = QueryResultTable::new()
13//!     .title("Query Results")
14//!     .columns(vec!["id", "name", "email"])
15//!     .row(vec!["1", "Alice", "alice@example.com"])
16//!     .row(vec!["2", "Bob", "bob@example.com"])
17//!     .timing_ms(12.34)
18//!     .max_width(80);
19//!
20//! // Plain mode output (pipe-delimited)
21//! println!("{}", table.render_plain());
22//!
23//! // Or use a different format
24//! println!("{}", table.render_plain_format(PlainFormat::Csv));
25//! ```
26
27use crate::theme::Theme;
28use std::time::Duration;
29
30/// Plain text output format for query results.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum PlainFormat {
33    /// Pipe-delimited format: `id|name|email` (default)
34    #[default]
35    Pipe,
36    /// CSV format with proper quoting
37    Csv,
38    /// JSON Lines format (one JSON object per row)
39    JsonLines,
40    /// JSON Array format (single array of objects)
41    JsonArray,
42}
43
44/// SQL value type for cell coloring.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum ValueType {
47    /// NULL value (gray, italic)
48    Null,
49    /// Boolean value (yellow)
50    Boolean,
51    /// Integer value (cyan)
52    Integer,
53    /// Float value (cyan)
54    Float,
55    /// String value (green)
56    #[default]
57    String,
58    /// Date value (magenta)
59    Date,
60    /// Time value (magenta)
61    Time,
62    /// Timestamp value (magenta)
63    Timestamp,
64    /// Binary/blob value (orange)
65    Binary,
66    /// JSON value (purple)
67    Json,
68    /// UUID value (orange)
69    Uuid,
70}
71
72impl ValueType {
73    /// Infer value type from a string value.
74    #[must_use]
75    pub fn infer(value: &str) -> Self {
76        let trimmed = value.trim();
77
78        // Check for NULL
79        if trimmed.eq_ignore_ascii_case("null") || trimmed.eq_ignore_ascii_case("<null>") {
80            return Self::Null;
81        }
82
83        // Check for boolean
84        if trimmed.eq_ignore_ascii_case("true") || trimmed.eq_ignore_ascii_case("false") {
85            return Self::Boolean;
86        }
87
88        // Check for binary blob marker
89        if trimmed.starts_with("[BLOB:") || trimmed.starts_with("<binary:") {
90            return Self::Binary;
91        }
92
93        // Check for JSON (starts with { or [)
94        if (trimmed.starts_with('{') && trimmed.ends_with('}'))
95            || (trimmed.starts_with('[') && trimmed.ends_with(']'))
96        {
97            return Self::Json;
98        }
99
100        // Check for UUID pattern (8-4-4-4-12)
101        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        // Check for date pattern (YYYY-MM-DD)
118        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        // Check for timestamp pattern (contains 'T' or date-like with time)
127        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        // Check for time pattern (HH:MM:SS)
135        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        // Check for integer
147        if trimmed.parse::<i64>().is_ok() {
148            return Self::Integer;
149        }
150
151        // Check for float
152        if trimmed.parse::<f64>().is_ok() {
153            return Self::Float;
154        }
155
156        // Default to string
157        Self::String
158    }
159
160    /// Get the ANSI color code for this value type from theme.
161    #[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/// A cell in the query result table.
177#[derive(Debug, Clone)]
178pub struct Cell {
179    /// The display value.
180    pub value: String,
181    /// The inferred or explicit value type.
182    pub value_type: ValueType,
183}
184
185impl Cell {
186    /// Create a cell with automatic type inference.
187    #[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    /// Create a cell with explicit type.
195    #[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    /// Create a NULL cell.
204    #[must_use]
205    pub fn null() -> Self {
206        Self {
207            value: "NULL".to_string(),
208            value_type: ValueType::Null,
209        }
210    }
211}
212
213/// A table for displaying query results.
214///
215/// Provides rich formatting for query result sets including type-based
216/// coloring, auto-sized columns, and multiple output formats.
217#[derive(Debug, Clone)]
218pub struct QueryResultTable {
219    /// Optional title for the table
220    title: Option<String>,
221    /// Column names
222    columns: Vec<String>,
223    /// Row data (each row is a vector of cells)
224    rows: Vec<Vec<Cell>>,
225    /// Query execution time in milliseconds
226    timing_ms: Option<f64>,
227    /// Maximum table width (for wrapping/truncation)
228    max_width: Option<usize>,
229    /// Maximum rows to display (rest shown as "... and N more")
230    max_rows: Option<usize>,
231    /// Show row numbers
232    show_row_numbers: bool,
233    /// Theme for styled output
234    theme: Option<Theme>,
235    /// Plain format for non-styled output
236    plain_format: PlainFormat,
237}
238
239/// Alias for `QueryResultTable` for simpler API.
240///
241/// This provides a more concise name for query results.
242pub type QueryResults = QueryResultTable;
243
244impl QueryResultTable {
245    /// Create a new empty query result table.
246    #[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    /// Create a query result table from column names and row data.
262    ///
263    /// This is a convenience constructor that directly sets columns and rows.
264    ///
265    /// # Example
266    ///
267    /// ```rust
268    /// use sqlmodel_console::renderables::QueryResultTable;
269    ///
270    /// let columns = vec!["id".to_string(), "name".to_string()];
271    /// let rows = vec![
272    ///     vec!["1".to_string(), "Alice".to_string()],
273    ///     vec!["2".to_string(), "Bob".to_string()],
274    /// ];
275    /// let table = QueryResultTable::from_data(columns, rows);
276    /// ```
277    #[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    /// Set the table title.
289    #[must_use]
290    pub fn title(mut self, title: impl Into<String>) -> Self {
291        self.title = Some(title.into());
292        self
293    }
294
295    /// Set the column names.
296    #[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    /// Add a row of string values (types inferred).
303    #[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    /// Add a row of cells (with explicit types).
311    #[must_use]
312    pub fn row_cells(mut self, cells: Vec<Cell>) -> Self {
313        self.rows.push(cells);
314        self
315    }
316
317    /// Add multiple rows at once.
318    #[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    /// Set the query timing in milliseconds.
331    #[must_use]
332    pub fn timing_ms(mut self, ms: f64) -> Self {
333        self.timing_ms = Some(ms);
334        self
335    }
336
337    /// Set the query timing from a Duration.
338    #[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    /// Set maximum table width.
345    #[must_use]
346    pub fn max_width(mut self, width: usize) -> Self {
347        self.max_width = Some(width);
348        self
349    }
350
351    /// Set maximum rows to display.
352    #[must_use]
353    pub fn max_rows(mut self, max: usize) -> Self {
354        self.max_rows = Some(max);
355        self
356    }
357
358    /// Enable row numbers.
359    #[must_use]
360    pub fn with_row_numbers(mut self) -> Self {
361        self.show_row_numbers = true;
362        self
363    }
364
365    /// Set the theme for styled output.
366    #[must_use]
367    pub fn theme(mut self, theme: Theme) -> Self {
368        self.theme = Some(theme);
369        self
370    }
371
372    /// Set the plain format for non-styled output.
373    #[must_use]
374    pub fn plain_format(mut self, format: PlainFormat) -> Self {
375        self.plain_format = format;
376        self
377    }
378
379    /// Get the number of rows.
380    #[must_use]
381    pub fn row_count(&self) -> usize {
382        self.rows.len()
383    }
384
385    /// Get the number of columns.
386    #[must_use]
387    pub fn column_count(&self) -> usize {
388        self.columns.len()
389    }
390
391    /// Calculate column widths based on content.
392    fn calculate_column_widths(&self) -> Vec<usize> {
393        let mut widths: Vec<usize> = self.columns.iter().map(|c| c.chars().count()).collect();
394
395        // Consider row number column if enabled
396        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        // Find max width for each column from data
402        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        // Apply max width constraint if set
412        if let Some(max_width) = self.max_width {
413            let total_padding = (widths.len() * 3) + 1; // | + space before/after each col
414            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)); // At least 3 chars
419            }
420        }
421
422        widths
423    }
424
425    /// Truncate a value to fit within width, adding "..." if needed.
426    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    /// Render as plain text using the configured format.
438    #[must_use]
439    pub fn render_plain(&self) -> String {
440        self.render_plain_format(self.plain_format)
441    }
442
443    /// Render as plain text using a specific format.
444    #[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    /// Render as pipe-delimited format.
455    fn render_pipe(&self) -> String {
456        let mut lines = Vec::new();
457
458        // Optional timing header
459        if let Some(ms) = self.timing_ms {
460            lines.push(format!("# {} rows in {:.2}ms", self.rows.len(), ms));
461        }
462
463        // Header row
464        let mut header = self.columns.join("|");
465        if self.show_row_numbers {
466            header = format!("#|{header}");
467        }
468        lines.push(header);
469
470        // Determine display rows
471        let display_rows = self.max_rows.unwrap_or(self.rows.len());
472        let truncated = self.rows.len() > display_rows;
473
474        // Data rows
475        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        // Truncation indicator
485        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    /// Render as CSV format.
496    fn render_csv(&self) -> String {
497        let mut lines = Vec::new();
498
499        // Header row
500        let header: Vec<String> = self.columns.iter().map(|c| Self::csv_escape(c)).collect();
501        lines.push(header.join(","));
502
503        // Determine display rows
504        let display_rows = self.max_rows.unwrap_or(self.rows.len());
505
506        // Data rows
507        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    /// Escape a value for CSV output.
516    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    /// Render as JSON Lines format.
526    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    /// Render as JSON Array format.
572    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    /// Render as styled text with ANSI colors and box drawing.
618    #[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        // Calculate total width
631        let total_width: usize = widths.iter().sum::<usize>() + (widths.len() * 3) + 1;
632
633        // Title bar
634        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        // Header row
675        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        // Header separator
695        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        // Determine display rows
702        let display_rows = self.max_rows.unwrap_or(self.rows.len());
703        let truncated = self.rows.len() > display_rows;
704
705        // Data rows
706        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                // Right-align numbers, left-align everything else
725                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        // Truncation indicator
750        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        // Bottom border
763        lines.push(format!(
764            "{border_color}╰{}╯{reset}",
765            "─".repeat(total_width - 2)
766        ));
767
768        lines.join("\n")
769    }
770
771    /// Render as JSON-serializable structure.
772    #[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}