feat: add Tauri v2 Rust backend with PostgreSQL support
Add Rust backend: AppState with connection pool management, TuskError handling, ConnectionConfig model, and all commands for connections, schema browsing, query execution, data CRUD, and CSV/JSON export via sqlx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
113
src-tauri/src/commands/queries.rs
Normal file
113
src-tauri/src/commands/queries.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use crate::error::{TuskError, TuskResult};
|
||||
use crate::models::query_result::QueryResult;
|
||||
use crate::state::AppState;
|
||||
use serde_json::Value;
|
||||
use sqlx::postgres::PgRow;
|
||||
use sqlx::{Column, Row, TypeInfo};
|
||||
use std::time::Instant;
|
||||
use tauri::State;
|
||||
|
||||
pub fn pg_value_to_json(row: &PgRow, index: usize) -> Value {
|
||||
let col = &row.columns()[index];
|
||||
let type_name = col.type_info().name();
|
||||
|
||||
macro_rules! try_get {
|
||||
($t:ty) => {
|
||||
match row.try_get::<Option<$t>, _>(index) {
|
||||
Ok(Some(v)) => return serde_json::to_value(v).unwrap_or(Value::Null),
|
||||
Ok(None) => return Value::Null,
|
||||
Err(_) => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match type_name {
|
||||
"BOOL" => try_get!(bool),
|
||||
"INT2" => try_get!(i16),
|
||||
"INT4" => try_get!(i32),
|
||||
"INT8" => try_get!(i64),
|
||||
"FLOAT4" => try_get!(f32),
|
||||
"FLOAT8" => try_get!(f64),
|
||||
"NUMERIC" => {
|
||||
try_get!(bigdecimal::BigDecimal);
|
||||
}
|
||||
"TEXT" | "VARCHAR" | "CHAR" | "BPCHAR" | "NAME" => try_get!(String),
|
||||
"JSON" | "JSONB" => try_get!(Value),
|
||||
"UUID" => try_get!(uuid::Uuid),
|
||||
"TIMESTAMP" => {
|
||||
try_get!(chrono::NaiveDateTime);
|
||||
}
|
||||
"TIMESTAMPTZ" => {
|
||||
try_get!(chrono::DateTime<chrono::Utc>);
|
||||
}
|
||||
"DATE" => try_get!(chrono::NaiveDate),
|
||||
"TIME" => try_get!(chrono::NaiveTime),
|
||||
"BYTEA" => {
|
||||
match row.try_get::<Option<Vec<u8>>, _>(index) {
|
||||
Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))),
|
||||
Ok(None) => return Value::Null,
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
"OID" => {
|
||||
match row.try_get::<Option<i32>, _>(index) {
|
||||
Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)),
|
||||
Ok(None) => return Value::Null,
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
"VOID" => return Value::Null,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Fallback: try as string
|
||||
match row.try_get::<Option<String>, _>(index) {
|
||||
Ok(Some(v)) => Value::String(v),
|
||||
Ok(None) => Value::Null,
|
||||
Err(_) => Value::String(format!("<unsupported type: {}>", type_name)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn execute_query(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
sql: String,
|
||||
) -> TuskResult<QueryResult> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let start = Instant::now();
|
||||
let rows = sqlx::query(&sql)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
let execution_time_ms = start.elapsed().as_millis();
|
||||
|
||||
let mut columns = Vec::new();
|
||||
let mut types = Vec::new();
|
||||
|
||||
if let Some(first_row) = rows.first() {
|
||||
for col in first_row.columns() {
|
||||
columns.push(col.name().to_string());
|
||||
types.push(col.type_info().name().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let result_rows: Vec<Vec<Value>> = rows
|
||||
.iter()
|
||||
.map(|row| (0..columns.len()).map(|i| pg_value_to_json(row, i)).collect())
|
||||
.collect();
|
||||
|
||||
let row_count = result_rows.len();
|
||||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
types,
|
||||
rows: result_rows,
|
||||
row_count,
|
||||
execution_time_ms,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user