pull/143/merge
kyoto7250 1 year ago committed by GitHub
commit ac99e52e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -111,6 +111,7 @@ If you want to add connections, you need to edit your config file. For more info
| <kbd>h</kbd>, <kbd>j</kbd>, <kbd>k</kbd>, <kbd>l</kbd> | Scroll left/down/up/right |
| <kbd>Ctrl</kbd> + <kbd>u</kbd>, <kbd>Ctrl</kbd> + <kbd>d</kbd> | Scroll up/down multiple lines |
| <kbd>g</kbd> , <kbd>G</kbd> | Scroll to top/bottom |
| <kbd>s</lbd> | Sort by selected column |
| <kbd>H</kbd>, <kbd>J</kbd>, <kbd>K</kbd>, <kbd>L</kbd> | Extend selection by one cell left/down/up/right |
| <kbd>y</kbd> | Copy a cell value |
| <kbd></kbd>, <kbd></kbd> | Move focus to left/right |

@ -163,7 +163,12 @@ impl App {
Ok(())
}
async fn update_record_table(&mut self) -> anyhow::Result<()> {
async fn update_record_table(
&mut self,
orders: Option<String>,
header_icons: Option<Vec<String>>,
hold_cursor_position: bool,
) -> anyhow::Result<()> {
if let Some((database, table)) = self.databases.tree().selected_table() {
let (headers, records) = self
.pool
@ -178,10 +183,16 @@ impl App {
} else {
Some(self.record_table.filter.input_str())
},
orders,
)
.await?;
self.record_table
.update(records, headers, database.clone(), table.clone());
self.record_table.update(
records,
self.concat_headers(headers, header_icons),
database.clone(),
table.clone(),
hold_cursor_position,
);
}
Ok(())
}
@ -231,10 +242,15 @@ impl App {
.pool
.as_ref()
.unwrap()
.get_records(&database, &table, 0, None)
.get_records(&database, &table, 0, None, None)
.await?;
self.record_table
.update(records, headers, database.clone(), table.clone());
self.record_table.update(
records,
headers,
database.clone(),
table.clone(),
false,
);
self.properties
.update(database.clone(), table.clone(), self.pool.as_ref().unwrap())
.await?;
@ -250,6 +266,17 @@ impl App {
return Ok(EventState::Consumed);
};
if key == self.config.key_config.sort_by_column
&& !self.record_table.table.headers.is_empty()
{
self.record_table.table.add_order();
let order_query = self.record_table.table.generate_order_query();
let header_icons = self.record_table.table.generate_header_icons();
self.update_record_table(order_query, Some(header_icons), true)
.await?;
return Ok(EventState::Consumed);
};
if key == self.config.key_config.copy {
if let Some(text) = self.record_table.table.selected_cells() {
copy_to_clipboard(text.as_str())?
@ -259,7 +286,10 @@ impl App {
if key == self.config.key_config.enter && self.record_table.filter_focused()
{
self.record_table.focus = crate::components::record_table::Focus::Table;
self.update_record_table().await?;
let order_query = self.record_table.table.generate_order_query();
let header_icons = self.record_table.table.generate_header_icons();
self.update_record_table(order_query, Some(header_icons), false)
.await?;
}
if self.record_table.table.eod {
@ -284,6 +314,7 @@ impl App {
} else {
Some(self.record_table.filter.input_str())
},
None,
)
.await?;
if !records.is_empty() {
@ -374,6 +405,24 @@ impl App {
}
Ok(EventState::NotConsumed)
}
fn concat_headers(
&self,
headers: Vec<String>,
header_icons: Option<Vec<String>>,
) -> Vec<String> {
if let Some(header_icons) = &header_icons {
let mut new_headers = vec![String::new(); headers.len()];
for (index, header) in headers.iter().enumerate() {
new_headers[index] = format!("{} {}", header, header_icons[index])
.trim()
.to_string();
}
return new_headers;
}
headers
}
}
#[cfg(test)]
@ -409,4 +458,25 @@ mod test {
);
assert_eq!(app.left_main_chunk_percentage, 15);
}
#[test]
fn test_concat_headers() {
let app = App::new(Config::default());
let headers = vec![
"ID".to_string(),
"NAME".to_string(),
"TIMESTAMP".to_string(),
];
let header_icons = vec!["".to_string(), "↑1".to_string(), "↓2".to_string()];
let concat_headers: Vec<String> = app.concat_headers(headers, Some(header_icons));
assert_eq!(
concat_headers,
vec![
"ID".to_string(),
"NAME ↑1".to_string(),
"TIMESTAMP ↓2".to_string()
]
)
}
}

@ -62,6 +62,13 @@ pub fn scroll_to_top_bottom(key: &KeyConfig) -> CommandText {
)
}
pub fn sort_by_column(key: &KeyConfig) -> CommandText {
CommandText::new(
format!("Sort by column [{}]", key.sort_by_column),
CMD_GROUP_TABLE,
)
}
pub fn expand_collapse(key: &KeyConfig) -> CommandText {
CommandText::new(
format!("Expand/Collapse [{},{}]", key.scroll_right, key.scroll_left,),

@ -77,6 +77,7 @@ impl PropertiesComponent {
columns.get(0).unwrap().fields(),
database.clone(),
table.clone(),
false,
);
}
self.constraint_table.reset();
@ -90,6 +91,7 @@ impl PropertiesComponent {
constraints.get(0).unwrap().fields(),
database.clone(),
table.clone(),
false,
);
}
self.foreign_key_table.reset();
@ -103,6 +105,7 @@ impl PropertiesComponent {
foreign_keys.get(0).unwrap().fields(),
database.clone(),
table.clone(),
false,
);
}
self.index_table.reset();
@ -116,6 +119,7 @@ impl PropertiesComponent {
indexes.get(0).unwrap().fields(),
database.clone(),
table.clone(),
false,
);
}
Ok(())

@ -39,8 +39,10 @@ impl RecordTableComponent {
headers: Vec<String>,
database: Database,
table: DTable,
hold_cusor_position: bool,
) {
self.table.update(rows, headers, database, table.clone());
self.table
.update(rows, headers, database, table.clone(), hold_cusor_position);
self.filter.table = Some(table);
}

@ -269,7 +269,7 @@ impl Component for SqlEditorComponent {
database,
table,
} => {
self.table.update(rows, headers, database, table);
self.table.update(rows, headers, database, table, false);
self.focus = Focus::Table;
self.query_result = None;
}

@ -17,11 +17,92 @@ use tui::{
};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, PartialEq)]
struct Order {
pub column_number: usize,
pub is_asc: bool,
}
impl Order {
pub fn new(column_number: usize, is_asc: bool) -> Self {
Self {
column_number,
is_asc,
}
}
fn query(&self) -> String {
let order = if self.is_asc { "ASC" } else { "DESC" };
return format!(
"{column} {order}",
column = self.column_number,
order = order
);
}
}
#[derive(PartialEq)]
struct OrderManager {
orders: Vec<Order>,
}
impl OrderManager {
fn new() -> Self {
Self { orders: vec![] }
}
fn generate_order_query(&mut self) -> Option<String> {
let order_query = self
.orders
.iter()
.map(|order| order.query())
.collect::<Vec<String>>();
if !order_query.is_empty() {
return Some("ORDER BY ".to_string() + &order_query.join(", "));
}
None
}
fn generate_header_icons(&mut self, header_length: usize) -> Vec<String> {
let mut header_icons = vec![String::new(); header_length];
for (index, order) in self.orders.iter().enumerate() {
let arrow = if order.is_asc { "↑" } else { "↓" };
header_icons[order.column_number - 1] =
format!("{arrow}{number}", arrow = arrow, number = index + 1);
}
header_icons
}
fn add_order(&mut self, selected_column: usize) {
let selected_column_number = selected_column + 1;
if let Some(position) = self
.orders
.iter()
.position(|order| order.column_number == selected_column_number)
{
if self.orders[position].is_asc {
self.orders[position].is_asc = false;
} else {
self.orders.remove(position);
}
} else {
let order = Order::new(selected_column_number, true);
self.orders.push(order);
}
}
}
pub struct TableComponent {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub eod: bool,
pub selected_row: TableState,
orders: OrderManager,
table: Option<(Database, DTable)>,
selected_column: usize,
selection_area_corner: Option<(usize, usize)>,
@ -36,6 +117,7 @@ impl TableComponent {
selected_row: TableState::default(),
headers: vec![],
rows: vec![],
orders: OrderManager::new(),
table: None,
selected_column: 0,
selection_area_corner: None,
@ -58,6 +140,7 @@ impl TableComponent {
headers: Vec<String>,
database: Database,
table: DTable,
hold_cusor_position: bool,
) {
self.selected_row.select(None);
if !rows.is_empty() {
@ -65,7 +148,11 @@ impl TableComponent {
}
self.headers = headers;
self.rows = rows;
self.selected_column = 0;
self.selected_column = if hold_cusor_position {
self.selected_column
} else {
0
};
self.selection_area_corner = None;
self.column_page_start = std::cell::Cell::new(0);
self.scroll = VerticalScroll::new(false, false);
@ -77,6 +164,7 @@ impl TableComponent {
self.selected_row.select(None);
self.headers = Vec::new();
self.rows = Vec::new();
self.orders = OrderManager::new();
self.selected_column = 0;
self.selection_area_corner = None;
self.column_page_start = std::cell::Cell::new(0);
@ -89,6 +177,18 @@ impl TableComponent {
self.selection_area_corner = None;
}
pub fn add_order(&mut self) {
self.orders.add_order(self.selected_column)
}
pub fn generate_order_query(&mut self) -> Option<String> {
self.orders.generate_order_query()
}
pub fn generate_header_icons(&mut self) -> Vec<String> {
self.orders.generate_header_icons(self.headers.len())
}
pub fn end(&mut self) {
self.eod = true;
}
@ -522,6 +622,7 @@ impl Component for TableComponent {
out.push(CommandInfo::new(command::extend_selection_by_one_cell(
&self.key_config,
)));
out.push(CommandInfo::new(command::sort_by_column(&self.key_config)));
}
fn event(&mut self, key: Key) -> Result<EventState> {
@ -568,7 +669,7 @@ impl Component for TableComponent {
#[cfg(test)]
mod test {
use super::{KeyConfig, TableComponent};
use super::{KeyConfig, Order, OrderManager, TableComponent};
use tui::layout::Constraint;
#[test]
@ -588,6 +689,78 @@ mod test {
assert_eq!(component.rows(1, 2), vec![vec!["1", "b"], vec!["2", "e"]],)
}
#[test]
fn test_query() {
let asc_order = Order::new(1, true);
let desc_order = Order::new(2, false);
assert_eq!(asc_order.query(), "1 ASC".to_string());
assert_eq!(desc_order.query(), "2 DESC".to_string());
}
#[test]
fn test_generate_order_query() {
let mut order_manager = OrderManager::new();
// If orders is empty, it should return None.
assert_eq!(order_manager.generate_order_query(), None);
order_manager.add_order(1);
order_manager.add_order(1);
order_manager.add_order(2);
assert_eq!(
order_manager.generate_order_query(),
Some("ORDER BY 2 DESC, 3 ASC".to_string())
)
}
#[test]
fn test_generate_header_icons() {
let mut order_manager = OrderManager::new();
assert_eq!(order_manager.generate_header_icons(1), vec![String::new()]);
order_manager.add_order(1);
order_manager.add_order(1);
order_manager.add_order(2);
assert_eq!(
order_manager.generate_header_icons(3),
vec![String::new(), "↓1".to_string(), "↑2".to_string()]
);
assert_eq!(
order_manager.generate_header_icons(4),
vec![
String::new(),
"↓1".to_string(),
"↑2".to_string(),
String::new()
]
);
}
#[test]
fn test_add_order() {
let mut order_manager = OrderManager::new();
// press first time, condition is asc.
order_manager.add_order(1);
assert_eq!(order_manager.orders, vec![Order::new(2, true)]);
// press twice times, condition is desc.
order_manager.add_order(1);
assert_eq!(order_manager.orders, vec![Order::new(2, false)]);
// press another column, this column is second order.
order_manager.add_order(2);
assert_eq!(
order_manager.orders,
vec![Order::new(2, false), Order::new(3, true)]
);
// press three times, removed.
order_manager.add_order(1);
assert_eq!(order_manager.orders, vec![Order::new(3, true)]);
}
#[test]
fn test_expand_selected_area_x_left() {
// before

@ -101,6 +101,7 @@ pub struct KeyConfig {
pub scroll_up_multiple_lines: Key,
pub scroll_to_top: Key,
pub scroll_to_bottom: Key,
pub sort_by_column: Key,
pub extend_selection_by_one_cell_left: Key,
pub extend_selection_by_one_cell_right: Key,
pub extend_selection_by_one_cell_up: Key,
@ -140,6 +141,7 @@ impl Default for KeyConfig {
scroll_up_multiple_lines: Key::Ctrl('u'),
scroll_to_top: Key::Char('g'),
scroll_to_bottom: Key::Char('G'),
sort_by_column: Key::Char('s'),
extend_selection_by_one_cell_left: Key::Char('H'),
extend_selection_by_one_cell_right: Key::Char('L'),
extend_selection_by_one_cell_down: Key::Char('J'),

@ -22,6 +22,7 @@ pub trait Pool: Send + Sync {
table: &Table,
page: u16,
filter: Option<String>,
orders: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn get_columns(
&self,

@ -228,21 +228,41 @@ impl Pool for MySqlPool {
table: &Table,
page: u16,
filter: Option<String>,
orders: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
let query = if let (Some(filter), Some(orders)) = (&filter, &orders) {
format!(
"SELECT * FROM `{database}`.`{table}` WHERE {filter} {orders} LIMIT {page}, {limit}",
database = database.name,
table = table.name,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE,
orders = orders
)
} else if let Some(filter) = filter {
format!(
"SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}",
database = database.name,
table = table.name,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
limit = RECORDS_LIMIT_PER_PAGE,
)
} else if let Some(orders) = orders {
format!(
"SELECT * FROM `{database}`.`{table}` {orders} LIMIT {page}, {limit}",
database = database.name,
table = table.name,
orders = orders,
page = page,
limit = RECORDS_LIMIT_PER_PAGE,
)
} else {
format!(
"SELECT * FROM `{}`.`{}` LIMIT {page}, {limit}",
database.name,
table.name,
"SELECT * FROM `{database}`.`{table}` LIMIT {page}, {limit}",
database = database.name,
table = table.name,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)

@ -245,8 +245,20 @@ impl Pool for PostgresPool {
table: &Table,
page: u16,
filter: Option<String>,
orders: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter.as_ref() {
let query = if let (Some(filter), Some(orders)) = (&filter, &orders) {
format!(
r#"SELECT * FROM "{database}"."{table_schema}"."{table}" WHERE {filter} {orders} LIMIT {limit} OFFSET {page}"#,
database = database.name,
table = table.name,
filter = filter,
orders = orders,
table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()),
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else if let Some(filter) = &filter {
format!(
r#"SELECT * FROM "{database}"."{table_schema}"."{table}" WHERE {filter} LIMIT {limit} OFFSET {page}"#,
database = database.name,
@ -256,6 +268,16 @@ impl Pool for PostgresPool {
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else if let Some(orders) = &orders {
format!(
r#"SELECT * FROM "{database}"."{table_schema}"."{table}" {orders} LIMIT {limit} OFFSET {page}"#,
database = database.name,
table = table.name,
orders = orders,
table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()),
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
r#"SELECT * FROM "{database}"."{table_schema}"."{table}" LIMIT {limit} OFFSET {page}"#,
@ -283,8 +305,14 @@ impl Pool for PostgresPool {
Err(_) => {
if json_records.is_none() {
json_records = Some(
self.get_json_records(database, table, page, filter.clone())
.await?,
self.get_json_records(
database,
table,
page,
filter.clone(),
orders.clone(),
)
.await?,
);
}
if let Some(json_records) = &json_records {
@ -479,8 +507,20 @@ impl PostgresPool {
table: &Table,
page: u16,
filter: Option<String>,
orders: Option<String>,
) -> anyhow::Result<Vec<serde_json::Value>> {
let query = if let Some(filter) = filter {
let query = if let (Some(filter), Some(orders)) = (&filter, &orders) {
format!(
r#"SELECT to_json("{table}".*) FROM "{database}"."{table_schema}"."{table}" WHERE {filter} {orders} LIMIT {limit} OFFSET {page}"#,
database = database.name,
table = table.name,
filter = filter,
orders = orders,
table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()),
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else if let Some(filter) = filter {
format!(
r#"SELECT to_json("{table}".*) FROM "{database}"."{table_schema}"."{table}" WHERE {filter} LIMIT {limit} OFFSET {page}"#,
database = database.name,
@ -490,6 +530,16 @@ impl PostgresPool {
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else if let Some(orders) = orders {
format!(
r#"SELECT to_json("{table}".*) FROM "{database}"."{table_schema}"."{table}" {orders} LIMIT {limit} OFFSET {page}"#,
database = database.name,
table = table.name,
orders = orders,
table_schema = table.schema.clone().unwrap_or_else(|| "public".to_string()),
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
r#"SELECT to_json("{table}".*) FROM "{database}"."{table_schema}"."{table}" LIMIT {limit} OFFSET {page}"#,

@ -230,19 +230,37 @@ impl Pool for SqlitePool {
table: &Table,
page: u16,
filter: Option<String>,
orders: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
let query = if let (Some(filter), Some(orders)) = (&filter, &orders) {
format!(
"SELECT * FROM `{table}` WHERE {filter} {orders} LIMIT {page}, {limit}",
table = table.name,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE,
orders = orders
)
} else if let Some(filter) = filter {
format!(
"SELECT * FROM `{table}` WHERE {filter} LIMIT {page}, {limit}",
table = table.name,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE,
)
} else if let Some(orders) = orders {
format!(
"SELECT * FROM `{table}`{orders} LIMIT {page}, {limit}",
table = table.name,
orders = orders,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
"SELECT * FROM `{}` LIMIT {page}, {limit}",
table.name,
"SELECT * FROM `{table}` LIMIT {page}, {limit}",
table = table.name,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)

Loading…
Cancel
Save