add sql editor

pull/84/head
Takayuki Maeda 3 years ago
parent c8ad6566c0
commit a0ca26ae87

@ -5,12 +5,27 @@ host = "localhost"
database = "world"
port = 3306
[[conn]]
type = "mysql"
user = "root"
host = "'127.0.0.1'"
database = "world"
port = 3306
[[conn]]
type = "mysql"
user = "root"
host = "localhost"
port = 3306
[[conn]]
type = "mysql"
user = "tako8ki"
host = "localhost"
port = 3306
database = "world"
password = "password"
[[conn]]
type = "postgres"
user = "postgres"
@ -22,6 +37,22 @@ type = "postgres"
user = "postgres"
host = "localhost"
port = 5432
password = "password"
database = "dvdrental"
[[conn]]
type = "postgres"
user = "hoge"
host = "localhost"
port = 5432
password = "hoge"
database = "dvdrental"
[[conn]]
type = "postgres"
user = "hoge"
host = "localhost"
port = 5432
database = "dvdrental"
[[conn]]

@ -8,7 +8,7 @@ use crate::{
components::tab::Tab,
components::{
command, ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent,
RecordTableComponent, TabComponent, TableComponent,
RecordTableComponent, SqlEditorComponent, TabComponent, TableComponent,
},
config::Config,
};
@ -30,6 +30,7 @@ pub struct App {
constraint_table: TableComponent,
foreign_key_table: TableComponent,
index_table: TableComponent,
sql_editor: SqlEditorComponent,
focus: Focus,
tab: TabComponent,
help: HelpComponent,
@ -51,6 +52,7 @@ impl App {
constraint_table: TableComponent::new(config.key_config.clone()),
foreign_key_table: TableComponent::new(config.key_config.clone()),
index_table: TableComponent::new(config.key_config.clone()),
sql_editor: SqlEditorComponent::new(config.key_config.clone()),
tab: TabComponent::new(config.key_config.clone()),
help: HelpComponent::new(config.key_config.clone()),
databases: DatabasesComponent::new(config.key_config.clone()),
@ -84,8 +86,7 @@ impl App {
.split(f.size());
self.databases
.draw(f, main_chunks[0], matches!(self.focus, Focus::DabataseList))
.unwrap();
.draw(f, main_chunks[0], matches!(self.focus, Focus::DabataseList))?;
let right_chunks = Layout::default()
.direction(Direction::Vertical)
@ -117,6 +118,10 @@ impl App {
self.index_table
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?
}
Tab::Sql => {
self.sql_editor
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?;
}
}
self.error.draw(f, Rect::default(), false)?;
self.help.draw(f, Rect::default(), false)?;
@ -437,6 +442,17 @@ impl App {
}
};
}
Tab::Sql => {
if self.sql_editor.event(key)?.is_consumed()
|| self
.sql_editor
.async_event(key, self.pool.as_ref().unwrap())
.await?
.is_consumed()
{
return Ok(EventState::Consumed);
};
}
};
}
}

@ -131,6 +131,10 @@ pub fn tab_indexes(key: &KeyConfig) -> CommandText {
CommandText::new(format!("Indexes [{}]", key.tab_indexes), CMD_GROUP_TABLE)
}
pub fn tab_sql_editor(key: &KeyConfig) -> CommandText {
CommandText::new(format!("SQL [{}]", key.tab_sql_editor), CMD_GROUP_TABLE)
}
pub fn toggle_tabs(key_config: &KeyConfig) -> CommandText {
CommandText::new(
format!(

@ -6,7 +6,7 @@ pub mod databases;
pub mod error;
pub mod help;
pub mod record_table;
pub mod syntax_text;
pub mod sql_editor;
pub mod tab;
pub mod table;
pub mod table_filter;
@ -25,6 +25,7 @@ pub use databases::DatabasesComponent;
pub use error::ErrorComponent;
pub use help::HelpComponent;
pub use record_table::RecordTableComponent;
pub use sql_editor::SqlEditorComponent;
pub use tab::TabComponent;
pub use table::TableComponent;
pub use table_filter::TableFilterComponent;
@ -34,6 +35,7 @@ pub use table_value::TableValueComponent;
#[cfg(debug_assertions)]
pub use debug::DebugComponent;
use crate::database::Pool;
use anyhow::Result;
use async_trait::async_trait;
use std::convert::TryInto;
@ -88,6 +90,14 @@ pub trait Component {
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
async fn async_event(
&mut self,
_key: crate::event::Key,
_pool: &Box<dyn Pool>,
) -> Result<EventState> {
Ok(EventState::NotConsumed)
}
fn focused(&self) -> bool {
false
}

@ -0,0 +1,126 @@
use super::{
compute_character_width, Component, EventState, StatefulDrawableComponent, TableComponent,
};
use crate::components::command::CommandInfo;
use crate::config::KeyConfig;
use crate::database::{ExecuteResult, Pool};
use crate::event::Key;
use crate::ui::syntax_text::SyntaxText;
use anyhow::Result;
use async_trait::async_trait;
use database_tree::Table;
use sqlx::query::Query;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Text,
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
struct QueryResult {
updated_rows: u64,
query: String,
}
pub enum Focus {
Editor,
Table,
}
pub struct SqlEditorComponent {
input: Vec<char>,
table: TableComponent,
query_result: Option<QueryResult>,
key_config: KeyConfig,
focus: Focus,
}
impl SqlEditorComponent {
pub fn new(key_config: KeyConfig) -> Self {
Self {
input: Vec::new(),
table: TableComponent::new(key_config.clone()),
focus: Focus::Editor,
query_result: None,
key_config,
}
}
}
impl StatefulDrawableComponent for SqlEditorComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(if matches!(self.focus, Focus::Table) {
vec![Constraint::Length(7), Constraint::Min(1)]
} else {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
})
.split(area);
let editor = Paragraph::new(self.input.iter().collect::<String>())
.block(
Block::default()
.borders(Borders::ALL)
.title("SQL Editor")
.style(if focused && matches!(self.focus, Focus::Editor) {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
}),
)
.wrap(Wrap { trim: true });
f.render_widget(editor, layout[0]);
self.table
.draw(f, layout[1], matches!(self.focus, Focus::Table))?;
Ok(())
}
}
#[async_trait]
impl Component for SqlEditorComponent {
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char(c) => {
self.input.push(c);
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(EventState::NotConsumed)
}
async fn async_event(&mut self, key: Key, pool: &Box<dyn Pool>) -> Result<EventState> {
if key == self.key_config.enter {
let query = self.input.iter().collect();
let result = pool.execute(&query).await?;
match result {
ExecuteResult::Read {
headers,
rows,
database,
table,
} => {
self.table.update(rows, headers, database, table);
self.focus = Focus::Table
}
ExecuteResult::Write { updated_rows } => {
self.query_result = Some(QueryResult {
updated_rows,
query: query.to_string(),
})
}
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
}

@ -20,6 +20,7 @@ pub enum Tab {
Constraints,
ForeignKeys,
Indexes,
Sql,
}
impl std::fmt::Display for Tab {
@ -52,6 +53,7 @@ impl TabComponent {
command::tab_constraints(&self.key_config).name,
command::tab_foreign_keys(&self.key_config).name,
command::tab_indexes(&self.key_config).name,
command::tab_sql_editor(&self.key_config).name,
]
}
}
@ -90,7 +92,7 @@ impl Component for TabComponent {
self.selected_tab = Tab::ForeignKeys;
return Ok(EventState::Consumed);
} else if key == self.key_config.tab_indexes {
self.selected_tab = Tab::Indexes;
self.selected_tab = Tab::Sql;
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)

@ -105,6 +105,7 @@ pub struct KeyConfig {
pub tab_indexes: Key,
pub extend_or_shorten_widget_width_to_right: Key,
pub extend_or_shorten_widget_width_to_left: Key,
pub tab_sql_editor: Key,
}
impl Default for KeyConfig {
@ -141,6 +142,7 @@ impl Default for KeyConfig {
tab_indexes: Key::Char('5'),
extend_or_shorten_widget_width_to_right: Key::Char('>'),
extend_or_shorten_widget_width_to_left: Key::Char('<'),
tab_sql_editor: Key::Char('6'),
}
}
}

@ -12,7 +12,8 @@ use database_tree::{Child, Database, Table};
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
#[async_trait]
pub trait Pool {
pub trait Pool: Send + Sync {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult>;
async fn get_databases(&self) -> anyhow::Result<Vec<Database>>;
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Child>>;
async fn get_records(
@ -45,6 +46,18 @@ pub trait Pool {
async fn close(&self);
}
pub enum ExecuteResult {
Read {
headers: Vec<String>,
rows: Vec<Vec<String>>,
database: Database,
table: Table,
},
Write {
updated_rows: u64,
},
}
pub trait TableRow: std::marker::Send {
fn fields(&self) -> Vec<String>;
fn columns(&self) -> Vec<String>;

@ -1,6 +1,6 @@
use crate::get_or_null;
use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use database_tree::{Child, Database, Table};
@ -146,6 +146,49 @@ impl TableRow for Index {
#[async_trait]
impl Pool for MySqlPool {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
let query = query.trim();
if query.starts_with("SELECT") || query.starts_with("select") {
let mut rows = sqlx::query(query).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|column| column.name().to_string())
.collect();
let mut new_row = vec![];
for column in row.columns() {
new_row.push(convert_column_value_to_string(&row, column)?)
}
records.push(new_row)
}
return Ok(ExecuteResult::Read {
headers,
rows: records,
database: Database {
name: "-".to_string(),
children: Vec::new(),
},
table: Table {
name: "-".to_string(),
create_time: None,
update_time: None,
engine: None,
schema: None,
},
});
}
let result = sqlx::query(query).execute(&self.pool).await?;
Ok(ExecuteResult::Write {
updated_rows: result.rows_affected(),
})
}
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SHOW DATABASES")
.fetch_all(&self.pool)

@ -1,6 +1,6 @@
use crate::get_or_null;
use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use database_tree::{Child, Database, Schema, Table};
@ -147,6 +147,47 @@ impl TableRow for Index {
#[async_trait]
impl Pool for PostgresPool {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
let query = query.trim();
if query.starts_with("SELECT") || query.starts_with("select") {
let mut rows = sqlx::query(query).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|column| column.name().to_string())
.collect();
let mut new_row = vec![];
for column in row.columns() {
new_row.push(convert_column_value_to_string(&row, column)?)
}
records.push(new_row)
}
return Ok(ExecuteResult::Read {
headers,
rows: records,
database: Database {
name: "-".to_string(),
children: Vec::new(),
},
table: Table {
name: "-".to_string(),
create_time: None,
update_time: None,
engine: None,
schema: None,
},
});
}
let result = sqlx::query(query).execute(&self.pool).await?;
Ok(ExecuteResult::Write {
updated_rows: result.rows_affected(),
})
}
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SELECT datname FROM pg_database")
.fetch_all(&self.pool)

@ -1,6 +1,6 @@
use crate::get_or_null;
use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::NaiveDateTime;
use database_tree::{Child, Database, Table};
@ -150,6 +150,47 @@ impl TableRow for Index {
#[async_trait]
impl Pool for SqlitePool {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
let query = query.trim();
if query.starts_with("SELECT") || query.starts_with("select") {
let mut rows = sqlx::query(query).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|column| column.name().to_string())
.collect();
let mut new_row = vec![];
for column in row.columns() {
new_row.push(convert_column_value_to_string(&row, column)?)
}
records.push(new_row)
}
return Ok(ExecuteResult::Read {
headers,
rows: records,
database: Database {
name: "-".to_string(),
children: Vec::new(),
},
table: Table {
name: "-".to_string(),
create_time: None,
update_time: None,
engine: None,
schema: None,
},
});
}
let result = sqlx::query(query).execute(&self.pool).await?;
Ok(ExecuteResult::Write {
updated_rows: result.rows_affected(),
})
}
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SELECT name FROM pragma_database_list")
.fetch_all(&self.pool)

@ -18,8 +18,8 @@ pub struct SyntaxText {
impl SyntaxText {
pub fn new(text: String) -> Self {
const SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_nonewlines();
const THEME_SET: ThemeSet = ThemeSet::load_defaults();
let SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_nonewlines();
let THEME_SET: ThemeSet = ThemeSet::load_defaults();
let mut state = ParseState::new(SYNTAX_SET.find_syntax_by_extension("sql").unwrap());
let highlighter = Highlighter::new(&THEME_SET.themes["base16-eighties.dark"]);

Loading…
Cancel
Save