Skip to main content

sqlk/table_viewer/data/
mod.rs

1use std::collections::HashMap;
2
3use anyhow::Result;
4use unicode_width::UnicodeWidthStr;
5
6use crate::config::Config;
7use crate::database::DatabaseManager;
8use crate::database::ForeignKeyInfo;
9
10use crate::database::QueryResult;
11use crate::table_viewer::ChartData;
12use crate::table_viewer::ColumnStats;
13use crate::table_viewer::ForeignKeyLookupResult;
14use crate::table_viewer::SearchState;
15
16#[derive(Debug)]
17pub struct CellInfo {
18    pub value: String,
19    pub column_name: String,
20    pub column_index: usize,
21    pub row_index: usize,
22    pub data_type: Option<String>,
23    pub is_null: bool,
24    pub value_length: usize,
25    pub duplicate_count: usize,
26    pub unique_values_in_column: usize,
27    pub percentage_of_total: f64,
28    pub foreign_key_info: Option<ForeignKeyLookupResult>,
29}
30
31#[derive(Debug, Clone, Copy)]
32pub struct CellPosition {
33    pub row: usize,
34    pub col: usize,
35}
36
37#[derive(Debug)]
38pub struct TableViewData {
39    pub headers: Vec<String>,
40    pub rows: Vec<Vec<String>>,
41    pub current_row_relative: usize,
42    pub current_col_relative: usize,
43    pub total_rows: usize,
44    pub total_cols: usize,
45    pub start_row: usize,
46    pub start_col: usize,
47    pub foreign_keys: HashMap<usize, ForeignKeyInfo>,
48    pub search_matches: Vec<CellPosition>,
49    pub execution_time: Option<std::time::Duration>,
50    pub column_types: Vec<String>,
51    pub show_chart: bool,
52    pub chart_display: Option<Vec<String>>,
53}
54#[derive(Debug)]
55pub struct TableViewer {
56    pub data: QueryResult,
57    pub current_row: usize,
58    pub current_col: usize,
59    pub scroll_offset_y: usize,
60    pub scroll_offset_x: usize,
61    pub search_state: SearchState,
62    pub foreign_keys: HashMap<usize, ForeignKeyInfo>,
63    pub col_width: usize,
64    pub movement_multiplier: Option<usize>,
65    pub column_stats: HashMap<usize, ColumnStats>,
66    pub show_chart: bool,
67    pub chart_data: Option<ChartData>,
68}
69
70impl TableViewer {
71    pub fn new(data: QueryResult, _config: &Config, db_manager: &DatabaseManager) -> Result<Self> {
72        let mut foreign_keys = HashMap::new();
73        let mut column_stats = HashMap::new();
74
75        for (idx, header) in data.headers.iter().enumerate() {
76            if let Ok(fk_info) = db_manager.get_foreign_key_info(header) {
77                foreign_keys.insert(idx, fk_info.clone());
78            }
79
80            let stats = Self::compute_column_stats(&data, idx);
81            column_stats.insert(idx, stats);
82        }
83
84        Ok(Self {
85            data,
86            current_row: 0,
87            current_col: 0,
88            scroll_offset_y: 0,
89            scroll_offset_x: 0,
90            search_state: SearchState::default(),
91            foreign_keys,
92            col_width: 20,
93            movement_multiplier: None,
94            column_stats,
95            show_chart: false,
96            chart_data: None,
97        })
98    }
99
100    pub fn get_column_type(&self, col_idx: usize) -> Option<&String> {
101        self.data.column_types.get(col_idx)
102    }
103
104    pub fn get_column_types(&self) -> &Vec<String> {
105        &self.data.column_types
106    }
107
108    pub fn get_current_cell_value(&self) -> Option<String> {
109        self.data
110            .rows
111            .get(self.current_row)?
112            .get(self.current_col)
113            .cloned()
114    }
115
116    pub fn get_visible_data(&self, width: u16, height: u16) -> TableViewData {
117        let available_width = width as usize;
118        let available_height = height as usize;
119
120        let col_width_with_padding = self.col_width + 3;
121        let max_visible_cols = (available_width / col_width_with_padding).max(1);
122
123        let start_col = self.scroll_offset_x;
124        let end_col = (start_col + max_visible_cols).min(self.data.headers.len());
125
126        let max_data_rows = available_height.saturating_sub(5);
127
128        let start_row = self.scroll_offset_y;
129        let end_row = (start_row + max_data_rows).min(self.data.rows.len());
130
131        let headers = self.data.headers[start_col..end_col].to_vec();
132        let rows = self.data.rows[start_row..end_row]
133            .iter()
134            .map(|row| {
135                row.get(start_col..end_col.min(row.len()))
136                    .unwrap_or_default()
137                    .to_vec()
138            })
139            .collect();
140
141        let column_types = self.data.column_types[start_col..end_col].to_vec();
142
143        TableViewData {
144            headers,
145            rows,
146            current_row_relative: self.current_row.saturating_sub(start_row),
147            current_col_relative: self.current_col.saturating_sub(start_col),
148            total_rows: self.data.rows.len(),
149            total_cols: self.data.headers.len(),
150            start_row,
151            start_col,
152            foreign_keys: self.get_visible_foreign_keys(start_col, end_col),
153            search_matches: self.get_visible_search_matches(start_row, end_row, start_col, end_col),
154            execution_time: self.data.execution_time,
155            column_types,
156            show_chart: self.show_chart,
157            chart_display: if self.show_chart {
158                Some(self.get_chart_display(available_width.saturating_sub(4)))
159            } else {
160                None
161            },
162        }
163    }
164
165    fn get_visible_foreign_keys(
166        &self,
167        start_col: usize,
168        end_col: usize,
169    ) -> HashMap<usize, ForeignKeyInfo> {
170        self.foreign_keys
171            .iter()
172            .filter(|(col_idx, _)| **col_idx >= start_col && **col_idx < end_col)
173            .map(|(col_idx, fk_info)| (col_idx - start_col, fk_info.clone()))
174            .collect()
175    }
176
177    pub fn format_cell(&self, content: &str) -> String {
178        let display_width = UnicodeWidthStr::width(content);
179
180        if display_width > self.col_width {
181            let mut truncated = String::new();
182            let mut current_width = 0;
183
184            for ch in content.chars() {
185                let char_width = UnicodeWidthStr::width(ch.to_string().as_str());
186                if current_width + char_width > self.col_width.saturating_sub(3) {
187                    truncated.push_str("...");
188                    break;
189                }
190                truncated.push(ch);
191                current_width += char_width;
192            }
193
194            let padding_needed = self
195                .col_width
196                .saturating_sub(UnicodeWidthStr::width(truncated.as_str()));
197            truncated + &" ".repeat(padding_needed)
198        } else {
199            let padding_needed = self.col_width.saturating_sub(display_width);
200            content.to_string() + &" ".repeat(padding_needed)
201        }
202    }
203
204    pub fn get_current_row_with_headers(&self) -> Option<(&Vec<String>, &Vec<String>)> {
205        if let Some(row) = self.data.rows.get(self.current_row) {
206            Some((&self.data.headers, row))
207        } else {
208            None
209        }
210    }
211}