Last active
May 29, 2026 00:10
-
-
Save etherealHero/8fd5afb3ded6a2369447a95c468a48c8 to your computer and use it in GitHub Desktop.
Egui SSMS alternative
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [package] | |
| name = "esa" | |
| version = "0.1.0" | |
| edition = "2024" | |
| [dependencies] | |
| eframe = "0.34.1" | |
| egui-async = "0.4.1" | |
| env_logger = { version = "0.11.10", features = ["auto-color", "humantime"] } | |
| log = "0.4.29" | |
| tokio = { version = "1.51.0", features = ["full"] } | |
| grid = "1.0.0" | |
| egui_extras = "=0.34.1" | |
| [dependencies.deadpool-tiberius] | |
| version = "0.1.9" | |
| features = [ | |
| "bigdecimal", | |
| "chrono", | |
| "rust_decimal", | |
| "time" | |
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release | |
| use eframe::egui; | |
| #[tokio::main] | |
| async fn main() -> eframe::Result { | |
| let native_options = eframe::NativeOptions::default(); | |
| eframe::run_native( | |
| "egui-async example", | |
| native_options, | |
| Box::new(|_cc| Ok(Box::new(MyApp::default()))), | |
| ) | |
| } | |
| type PoolManager = deadpool_tiberius::deadpool::managed::Pool<deadpool_tiberius::Manager>; | |
| struct MyApp { | |
| db_query_result: egui_async::Bind<Vec<grid::Grid<String>>, String>, | |
| pool: egui_async::Bind<PoolManager, String>, | |
| query: String, | |
| } | |
| impl Default for MyApp { | |
| fn default() -> Self { | |
| Self { | |
| db_query_result: egui_async::Bind::new(true), | |
| pool: egui_async::Bind::new(true), | |
| query: "select top 30 * from acv_Auction (nolock) | |
| select top 5 idRecord from acv_Auction (nolock) | |
| select top 30 * from acv_Auction (nolock) | |
| " | |
| .to_string(), | |
| } | |
| } | |
| } | |
| impl eframe::App for MyApp { | |
| fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { | |
| ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>(); // <-- REQUIRED | |
| } | |
| fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { | |
| // ui.ctx().all_styles_mut(|s| { | |
| // s.spacing.scroll = egui::style::ScrollStyle::solid(); // unstable | |
| // }); | |
| egui::CentralPanel::default().show_inside(ui, |ui| { | |
| ui.heading("egui-async SQL Demo"); | |
| if let Some(res) = self.pool.read_or_request(db_connect) { | |
| if let Err(err) = res { | |
| ui.colored_label(egui::Color32::RED, format!("Connect failed: {err}")); | |
| } | |
| } else { | |
| ui.label("Connecting to DB... "); | |
| } | |
| let button = ui.add_enabled( | |
| self.pool.is_ok() && !self.db_query_result.is_pending(), | |
| egui::Button::new("Execute Query"), | |
| ); | |
| if button.clicked() { | |
| if let Some(Ok(pool)) = self.pool.read() { | |
| let pool = pool.clone(); | |
| let query = self.query.clone(); | |
| self.db_query_result.refresh(execute_query(pool, query)); | |
| } | |
| } | |
| ui.with_layout(egui::Layout::default().with_cross_justify(true), |ui| { | |
| ui.text_edit_multiline(&mut self.query); | |
| match self.db_query_result.state() { | |
| egui_async::StateWithData::Idle => {} | |
| egui_async::StateWithData::Pending => { | |
| ui.horizontal(|ui| { | |
| ui.spinner(); | |
| ui.label("Executing query..."); | |
| }); | |
| } | |
| egui_async::StateWithData::Finished(datasets) => { | |
| ui.label("Result:"); | |
| egui::ScrollArea::vertical() | |
| .content_margin(egui::Margin { | |
| right: 14, | |
| ..Default::default() | |
| }) | |
| .show(ui, |ui| { | |
| for (i, dataset) in datasets.iter().enumerate() { | |
| egui::Frame::group(ui.style()).show(ui, |ui| { | |
| egui::ScrollArea::horizontal() | |
| .id_salt(format!("table_dataset_scroll_area_{i}")) | |
| .show(ui, |ui| { | |
| ui.set_min_width(0.0); | |
| table(ui, dataset, format!("table_dataset_{i}")); | |
| }); | |
| }); | |
| } | |
| }); | |
| ui.add_space(16.0); | |
| } | |
| egui_async::StateWithData::Failed(err) => { | |
| ui.colored_label(egui::Color32::RED, format!("Error:\n{err}")); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| fn table(ui: &mut egui::Ui, dataset: &grid::Grid<String>, id_salt: impl std::hash::Hash) { | |
| use egui_extras::{Column, TableBuilder}; | |
| let text_height = egui::TextStyle::Body | |
| .resolve(ui.style()) | |
| .size | |
| .max(ui.spacing().interact_size.y); | |
| let mut table = TableBuilder::new(ui) | |
| .striped(true) | |
| .resizable(true) | |
| .id_salt(id_salt) | |
| .auto_shrink([false, true]) | |
| .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) | |
| .min_scrolled_height(100.0) | |
| .max_scroll_height(300.0); | |
| table = table.column(Column::auto().at_least(40.0).clip(true).resizable(true)); // npp | |
| for _ in 0..dataset.cols() { | |
| table = table.column(Column::auto().at_least(40.0).clip(true).resizable(true)); // data | |
| } | |
| table = table.column(Column::remainder().at_least(14.0).resizable(false)); // phantom col | |
| table = table.sense(egui::Sense::click()); | |
| table | |
| .header(20.0, |mut header| { | |
| header.col(|ui| { | |
| ui.strong("№"); | |
| }); | |
| for col_idx in 0..dataset.cols() { | |
| let cell = &dataset[(0, col_idx)]; | |
| header.col(|ui| { | |
| ui.strong(cell); | |
| }); | |
| } | |
| header.col(|_ui| {}); // phantom col | |
| }) | |
| .body(|body| { | |
| body.rows(text_height, dataset.rows() - 1, |mut row| { | |
| let row_idx = row.index(); | |
| row.set_overline(row_idx.is_multiple_of(10)); | |
| row.col(|ui| { | |
| ui.label((row_idx + 1).to_string()); | |
| }); | |
| for col_idx in 0..dataset.cols() { | |
| row.col(|ui| { | |
| ui.label(&dataset[(row_idx + 1, col_idx)]); | |
| }); | |
| } | |
| row.col(|_ui| {}); // phantom col | |
| }); | |
| }); | |
| } | |
| async fn execute_query( | |
| pool: PoolManager, | |
| query: String, | |
| ) -> Result<Vec<grid::Grid<String>>, String> { | |
| let results = pool | |
| .get() | |
| .await | |
| .map_err(|e| format!("Connection error: {}", e))? | |
| .simple_query(&query) | |
| .await | |
| .map_err(|e| format!("Query execution error: {e}"))? | |
| .into_results() | |
| .await | |
| .map_err(|e| format!("Fetching results error: {e}"))?; | |
| let mut datasets = Vec::<grid::Grid<String>>::with_capacity(results.len()); | |
| for result in &results { | |
| let ds_width = result[0].columns().len(); | |
| let mut dataset = grid::Grid::with_capacity(result.len(), ds_width); | |
| let header: Vec<_> = result[0] | |
| .columns() | |
| .iter() | |
| .enumerate() | |
| .map(|(col_idx, col)| match col.name().is_empty() { | |
| true => format!("Field{col_idx}"), | |
| false => col.name().to_string(), | |
| }) | |
| .collect(); | |
| dataset.push_row(header); | |
| for row in result { | |
| dataset.push_row( | |
| row.cells() | |
| .map(|(_col, value)| column_data_to_string(value)) | |
| .collect(), | |
| ); | |
| } | |
| datasets.push(dataset); | |
| } | |
| Ok(datasets) | |
| } | |
| async fn db_connect() -> Result<PoolManager, String> { | |
| deadpool_tiberius::Manager::new() | |
| .host("host") | |
| .database("database") | |
| .authentication(deadpool_tiberius::tiberius::AuthMethod::Integrated) | |
| .trust_cert() | |
| .wait_timeout(std::time::Duration::from_secs(5)) | |
| .create_pool() | |
| .map_err(|e| format!("Pool creation error: {}", e)) | |
| } | |
| fn column_data_to_string(value: &deadpool_tiberius::tiberius::ColumnData<'static>) -> String { | |
| use deadpool_tiberius::tiberius::{ColumnData as D, FromSql, time::chrono}; | |
| fn null() -> String { | |
| "NULL".to_string() | |
| } | |
| fn conv_err() -> String { | |
| "conversion error".to_string() | |
| } | |
| fn opt<T: ToString>(v: Option<T>) -> String { | |
| v.map(|v| v.to_string()).unwrap_or_else(null) | |
| } | |
| fn date_to_string<T: ToString + for<'a> FromSql<'a>>(value: &D<'static>) -> String { | |
| T::from_sql(value) | |
| .ok() | |
| .flatten() | |
| .map(|v| v.to_string().split_at(19).0.to_string()) | |
| .unwrap_or_else(conv_err) | |
| } | |
| match value { | |
| D::Bit(v) => opt(*v), | |
| D::U8(v) => opt(*v), | |
| D::I16(v) => opt(*v), | |
| D::I32(v) => opt(*v), | |
| D::I64(v) => opt(*v), | |
| D::F32(v) => opt(*v), | |
| D::F64(v) => opt(*v), | |
| D::String(v) => opt(v.as_deref()), | |
| D::Guid(v) => opt(*v), | |
| D::Numeric(v) => opt(*v), | |
| D::Xml(v) => opt(v.as_deref()), | |
| D::Binary(v) => v | |
| .as_ref() | |
| .map(|b| { | |
| format!( | |
| "0x{}", | |
| b.iter().map(|b| format!("{:02X}", b)).collect::<String>() | |
| ) | |
| }) | |
| .unwrap_or_else(null), | |
| D::DateTime(_) | D::SmallDateTime(_) | D::DateTime2(_) => { | |
| date_to_string::<chrono::NaiveDateTime>(value) | |
| } | |
| D::Date(_) => date_to_string::<chrono::NaiveDate>(value), | |
| D::Time(_) => date_to_string::<chrono::NaiveTime>(value), | |
| D::DateTimeOffset(_) => chrono::DateTime::<chrono::FixedOffset>::from_sql(value) | |
| .ok() | |
| .flatten() | |
| .map(|dt| dt.to_string()) | |
| .unwrap_or_else(conv_err), | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use anyhow::{Context, Result, anyhow, bail, ensure}; | |
| use deadpool_tiberius as dt; | |
| use std::{ops::ControlFlow as F, path::PathBuf, sync::Arc}; | |
| use tracing::{debug, error, info, warn}; | |
| type Pool = Arc<tokio::sync::Mutex<Option<dt::Pool>>>; | |
| async fn init_pool(ado_connection_string: &str) -> Result<dt::Pool> { | |
| let manager = dt::Manager::from_ado_string(ado_connection_string)?.trust_cert(); | |
| let with_timeout = manager.wait_timeout(std::time::Duration::from_secs(5)); | |
| with_timeout.create_pool().context("init pool error") | |
| } | |
| async fn query(pool: Pool, query: &str) -> Result<Vec<dt::tiberius::Row>> { | |
| let pool = pool.try_lock()?.as_ref().cloned(); | |
| let pool = pool.context("connection pool not initialized")?; | |
| let pool_timeouts = dt::deadpool::managed::Timeouts { | |
| wait: Some(std::time::Duration::from_secs(10)), | |
| create: Some(std::time::Duration::from_secs(10)), | |
| recycle: Some(std::time::Duration::from_secs(10)), | |
| }; | |
| info!("execute query..."); | |
| let mut conn = pool.timeout_get(&pool_timeouts).await?; | |
| let running_query = async { conn.simple_query(query).await?.into_first_result().await }; | |
| let timeout_duration = std::time::Duration::from_secs(60); | |
| let Ok(result) = tokio::time::timeout(timeout_duration, running_query).await else { | |
| dt::deadpool::managed::Object::take(conn).close().await?; | |
| bail!("execute query timeout") | |
| }; | |
| Ok(result.inspect(|r| info!("execute query success: {} rows selected", r.len()))?) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment