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}