use crate::db::sql_guard::ensure_readonly_sql; use crate::error::{TuskError, TuskResult}; use crate::models::query_result::QueryResult; use crate::state::{AppState, DbFlavor}; use serde_json::Value; use sqlx::postgres::PgRow; use sqlx::{Column, Row, TypeInfo}; use std::sync::Arc; 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::, _>(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); } "DATE" => try_get!(chrono::NaiveDate), "TIME" => try_get!(chrono::NaiveTime), "INTERVAL" => match row.try_get::, _>(index) { Ok(Some(v)) => return Value::String(format_pg_interval(&v)), Ok(None) => return Value::Null, Err(_) => {} }, "BYTEA" => match row.try_get::>, _>(index) { Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))), Ok(None) => return Value::Null, Err(_) => {} }, "OID" => match row.try_get::, _>(index) { Ok(Some(v)) => return Value::Number(serde_json::Number::from(v)), Ok(None) => return Value::Null, Err(_) => {} }, "VOID" => return Value::Null, // Array types (PG prefixes array type names with underscore) "_BOOL" => try_get!(Vec), "_INT2" => try_get!(Vec), "_INT4" => try_get!(Vec), "_INT8" => try_get!(Vec), "_FLOAT4" => try_get!(Vec), "_FLOAT8" => try_get!(Vec), "_TEXT" | "_VARCHAR" | "_CHAR" | "_BPCHAR" | "_NAME" => try_get!(Vec), "_UUID" => try_get!(Vec), "_JSON" | "_JSONB" => try_get!(Vec), _ => {} } // Fallback: try as string match row.try_get::, _>(index) { Ok(Some(v)) => Value::String(v), Ok(None) => Value::Null, Err(_) => Value::String(format!("", type_name)), } } /// Render a PostgreSQL INTERVAL the same way `psql` does: /// "1 year 2 mons 3 days 04:05:06.789012" /// Components are emitted only when non-zero; the time component fires /// whenever microseconds != 0 OR everything else is zero. fn format_pg_interval(iv: &sqlx::postgres::types::PgInterval) -> String { let years = iv.months / 12; let months = iv.months % 12; let mut parts: Vec = Vec::new(); if years != 0 { parts.push(format!("{} year{}", years, if years.abs() == 1 { "" } else { "s" })); } if months != 0 { parts.push(format!("{} mon{}", months, if months.abs() == 1 { "" } else { "s" })); } if iv.days != 0 { parts.push(format!("{} day{}", iv.days, if iv.days.abs() == 1 { "" } else { "s" })); } if iv.microseconds != 0 || parts.is_empty() { let total_us = iv.microseconds.unsigned_abs(); let total_seconds = total_us / 1_000_000; let micros = (total_us % 1_000_000) as u32; let h = total_seconds / 3600; let m = (total_seconds / 60) % 60; let s = total_seconds % 60; let sign = if iv.microseconds < 0 { "-" } else { "" }; let time_part = if micros == 0 { format!("{}{:02}:{:02}:{:02}", sign, h, m, s) } else { format!( "{}{:02}:{:02}:{:02}.{:06}", sign, h, m, s, micros ) }; parts.push(time_part); } parts.join(" ") } pub async fn execute_query_core( state: &AppState, connection_id: &str, sql: &str, ) -> TuskResult { let read_only = state.is_read_only(connection_id).await; let flavor = state.get_flavor(connection_id).await; if read_only { ensure_readonly_sql(sql)?; } if matches!(flavor, DbFlavor::ClickHouse) { let client = state.get_ch_client(connection_id).await?; return client.execute_query(sql, read_only).await; } let pools = state.pools.read().await; let pool = pools .get(connection_id) .ok_or_else(|| TuskError::NotConnected(connection_id.to_string()))?; let start = Instant::now(); let rows = if read_only { let mut tx = pool.begin().await.map_err(TuskError::Database)?; sqlx::query("SET TRANSACTION READ ONLY") .execute(&mut *tx) .await .map_err(TuskError::Database)?; let result = sqlx::query(sql) .fetch_all(&mut *tx) .await .map_err(TuskError::Database); tx.rollback().await.map_err(TuskError::Database)?; result? } else { sqlx::query(sql) .fetch_all(pool) .await .map_err(TuskError::Database)? }; let execution_time_ms = start.elapsed().as_millis() as u64; 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> = 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, }) } #[tauri::command] pub async fn execute_query( state: State<'_, Arc>, connection_id: String, sql: String, ) -> TuskResult { execute_query_core(&state, &connection_id, &sql).await } #[cfg(test)] mod tests { use super::format_pg_interval; use sqlx::postgres::types::PgInterval; #[test] fn interval_zero_renders_as_zero_time() { let iv = PgInterval { months: 0, days: 0, microseconds: 0 }; assert_eq!(format_pg_interval(&iv), "00:00:00"); } #[test] fn interval_pure_time_micros() { // 1h 30m let iv = PgInterval { months: 0, days: 0, microseconds: 90 * 60 * 1_000_000 }; assert_eq!(format_pg_interval(&iv), "01:30:00"); } #[test] fn interval_days_only() { let iv = PgInterval { months: 0, days: 3, microseconds: 0 }; assert_eq!(format_pg_interval(&iv), "3 days"); } #[test] fn interval_one_day() { let iv = PgInterval { months: 0, days: 1, microseconds: 0 }; assert_eq!(format_pg_interval(&iv), "1 day"); } #[test] fn interval_mixed_components() { // 1 year 2 mons 3 days 04:05:06 let iv = PgInterval { months: 14, days: 3, microseconds: ((4 * 3600) + (5 * 60) + 6) * 1_000_000, }; assert_eq!(format_pg_interval(&iv), "1 year 2 mons 3 days 04:05:06"); } #[test] fn interval_negative_time() { let iv = PgInterval { months: 0, days: 0, microseconds: -3_600_000_000 }; assert_eq!(format_pg_interval(&iv), "-01:00:00"); } #[test] fn interval_with_microseconds_fraction() { let iv = PgInterval { months: 0, days: 0, microseconds: 1_500_000 }; assert_eq!(format_pg_interval(&iv), "00:00:01.500000"); } }