blob: 09240f77f5fcc045bfd9a6d5593b8eae86bb7412 [file]
use crate::config::Environment;
use crate::connection::socks5_forwarder::Socks5Forwarder;
use crate::error::{DorisError, DorisResult};
use mysql_async::prelude::*;
use mysql_async::{Conn, Opts, OptsBuilder, Row, Value as MysqlValue};
use serde_json::{Map, Value};
pub struct MysqlConnection {
conn: Conn,
// Kept alive for the lifetime of the MySQL connection when SOCKS5 is in use.
// Dropping it aborts the listener task; must outlive `conn`.
#[allow(dead_code)]
forwarder: Option<Socks5Forwarder>,
}
/// Result of a SQL query execution.
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Map<String, Value>>,
}
impl MysqlConnection {
/// Connect to a Doris instance. Routes through SOCKS5 when `env.socks5`
/// is set, via a loopback forwarder (mysql_async 0.34 has no native proxy hook).
pub async fn connect(env: &Environment) -> DorisResult<Self> {
let (dial_host, dial_port, forwarder, error_hint) = if let Some(s5) = &env.socks5 {
let fwd = Socks5Forwarder::spawn(s5, env.host.clone(), env.mysql_port).await?;
let port = fwd.local_addr.port();
let hint = format!(" via socks5://{}:{}", s5.host, s5.port);
("127.0.0.1".to_string(), port, Some(fwd), hint)
} else {
(env.host.clone(), env.mysql_port, None, String::new())
};
let opts = OptsBuilder::default()
.ip_or_hostname(&dial_host)
.tcp_port(dial_port)
.user(Some(&env.user))
.pass(Some(&env.password))
.prefer_socket(false);
let pool = mysql_async::Pool::new(Opts::from(opts));
let conn = pool.get_conn().await.map_err(|e| {
DorisError::connection_with_source(
format!(
"Failed to connect to {}:{} as '{}'{}",
env.host, env.mysql_port, env.user, error_hint
),
e,
)
})?;
let mut session = MysqlConnection { conn, forwarder };
// Run the post-connect init directive (e.g. `USE @<compute-group>`),
// sourced from `--init-sql` / `DORIS_INIT_SQL`. Never persisted.
if let Some(directive) = &env.cluster_routing_directive {
session.exec(directive).await?;
}
Ok(session)
}
/// Execute a SQL query and return structured results.
pub async fn query(&mut self, sql: &str) -> DorisResult<QueryResult> {
let rows: Vec<Row> = self
.conn
.query(sql)
.await
.map_err(|e| DorisError::sql(format!("{e}")))?;
if rows.is_empty() {
return Ok(QueryResult {
columns: Vec::new(),
rows: Vec::new(),
});
}
// Extract column names from first row
let columns: Vec<String> = rows[0]
.columns_ref()
.iter()
.map(|c| c.name_str().to_string())
.collect();
let mut result_rows = Vec::with_capacity(rows.len());
for row in &rows {
let mut map = Map::new();
for (i, col_name) in columns.iter().enumerate() {
let value = mysql_value_to_json(row.as_ref(i));
map.insert(col_name.clone(), value);
}
result_rows.push(map);
}
Ok(QueryResult {
columns,
rows: result_rows,
})
}
/// Execute a statement that doesn't return results (SET, USE, etc.).
pub async fn exec(&mut self, sql: &str) -> DorisResult<()> {
self.conn
.query_drop(sql)
.await
.map_err(|e| DorisError::sql(format!("{e}")))?;
Ok(())
}
/// Get the last query ID from the current session.
pub async fn last_query_id(&mut self) -> DorisResult<String> {
let result = self.query("SELECT last_query_id()").await?;
if let Some(row) = result.rows.first() {
if let Some(Value::String(qid)) = row.values().next() {
return Ok(qid.clone());
}
}
Ok(String::new())
}
/// Test connection by running SELECT 1, returns latency in ms.
pub async fn ping(&mut self) -> DorisResult<u128> {
let start = std::time::Instant::now();
self.query("SELECT 1").await?;
Ok(start.elapsed().as_millis())
}
}
/// Convert a mysql_async Value to serde_json Value.
fn mysql_value_to_json(value: Option<&MysqlValue>) -> Value {
match value {
None | Some(MysqlValue::NULL) => Value::Null,
Some(MysqlValue::Bytes(bytes)) => {
let s = String::from_utf8_lossy(bytes).to_string();
// Try to parse as number first
if let Ok(n) = s.parse::<i64>() {
Value::Number(n.into())
} else if let Ok(n) = s.parse::<f64>() {
serde_json::Number::from_f64(n)
.map(Value::Number)
.unwrap_or(Value::String(s))
} else {
Value::String(s)
}
}
Some(MysqlValue::Int(n)) => Value::Number((*n).into()),
Some(MysqlValue::UInt(n)) => Value::Number((*n).into()),
Some(MysqlValue::Float(n)) => serde_json::Number::from_f64(*n as f64)
.map(Value::Number)
.unwrap_or(Value::Null),
Some(MysqlValue::Double(n)) => serde_json::Number::from_f64(*n)
.map(Value::Number)
.unwrap_or(Value::Null),
Some(other) => Value::String(format!("{other:?}")),
}
}