Skip to content

Instantly share code, notes, and snippets.

@etherealHero
Last active May 29, 2026 00:10
Show Gist options
  • Select an option

  • Save etherealHero/8fd5afb3ded6a2369447a95c468a48c8 to your computer and use it in GitHub Desktop.

Select an option

Save etherealHero/8fd5afb3ded6a2369447a95c468a48c8 to your computer and use it in GitHub Desktop.
Egui SSMS alternative
[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"
]
#![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),
}
}
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