pub mod actions;
pub mod caching_sha2;
pub mod packet_limit;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use actions::{MysqlProtocol, MYSQL_QUERY_EVENT};
use anyhow::Result;
use async_trait::async_trait;
use opensrv_mysql::{
AsyncMysqlIntermediary, AsyncMysqlShim, Column, ColumnFlags, ColumnType, ErrorKind, InitWriter,
OkResponse, ParamParser, QueryResultWriter, RowWriter, StatementMetaWriter, StatusFlags,
};
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, error, trace, warn};
const HANDSHAKE_RESPONSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_COMMANDS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const LINGER_DRAIN_BYTES: usize = 8 * 1024 * 1024;
const LINGER_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const CONNECTION_CAP_REFUSAL: &[u8] = b"\x1d\x00\x00\x00\xff\x10\x04#08004Too many connections";
const MAX_PREPARED_STATEMENTS: usize = 4096;
const MAX_PLACEHOLDERS: usize = 1024;
static PLACEHOLDER_COLUMNS: std::sync::LazyLock<Vec<Column>> = std::sync::LazyLock::new(|| {
(0..MAX_PLACEHOLDERS)
.map(|_| Column {
table: String::new(),
column: "?".to_string(),
coltype: ColumnType::MYSQL_TYPE_VAR_STRING,
colflags: ColumnFlags::empty(),
})
.collect()
});
pub struct MysqlServer {
llm_client: OllamaClient,
app_state: Arc<AppState>,
_status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
}
impl MysqlServer {
pub fn new(
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
) -> Self {
Self {
llm_client,
app_state,
_status_tx: status_tx,
server_id,
}
}
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
_send_first: bool,
server_id: crate::state::ServerId,
) -> Result<SocketAddr> {
let listener = TcpListener::bind(listen_addr).await?;
let actual_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("MySQL server listening on {}", actual_addr));
let server = Arc::new(MysqlServer::new(
llm_client,
app_state.clone(),
status_tx.clone(),
Some(server_id),
));
let status_tx_clone = status_tx.clone();
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"MySQL",
Some(&status_tx),
)
.await
{
Ok((stream, addr, permit)) => {
Log::new(Some(&status_tx)).info(format!("MySQL connection from {}", addr));
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(actual_addr);
let handler = MysqlHandler::new(
connection_id,
server.llm_client.clone(),
server.app_state.clone(),
status_tx.clone(),
server.server_id,
addr,
);
if let Some(server_id) = server.server_id {
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr: addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
server
.app_state
.add_connection_to_server(server_id, conn_state)
.await;
}
let conn_state_owner = server.app_state.clone();
let conn_server_id = server.server_id;
let status_tx_conn = status_tx.clone();
let conn_handle = tokio::spawn(async move {
let _permit = permit;
let (reader, mut writer) = tokio::io::split(stream);
let reader =
crate::server::accept_bounded::IdleTimeoutReader::with_first(
reader,
HANDSHAKE_RESPONSE_TIMEOUT,
IDLE_BETWEEN_COMMANDS_TIMEOUT,
);
let trip = Arc::new(packet_limit::PacketLimitTrip::default());
let mut reader =
packet_limit::PacketLimitReader::new(reader, trip.clone());
let outcome = {
let mut writer = caching_sha2::FastAuthWriter::new(
&mut writer,
handler.fast_auth_gate(),
);
AsyncMysqlIntermediary::run_on(handler, &mut reader, &mut writer)
.await
};
if trip.tripped() {
Self::refuse_oversized_packet(
connection_id,
addr,
&trip,
reader.into_inner(),
&mut writer,
&status_tx_conn,
)
.await;
} else if let Err(e) = outcome {
error!("MySQL connection error: {:?}", e);
}
if let Some(server_id) = conn_server_id {
conn_state_owner
.close_connection_on_server(server_id, connection_id)
.await;
}
});
if let Some(server_id) = conn_server_id {
app_state.register_server_task(server_id, conn_handle).await;
}
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!("MySQL accept error: {}", e));
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
Ok(actual_addr)
}
async fn refuse_oversized_packet<R, W>(
connection_id: ConnectionId,
remote_addr: SocketAddr,
trip: &packet_limit::PacketLimitTrip,
mut reader: R,
writer: &mut W,
status_tx: &mpsc::UnboundedSender<String>,
) where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let declared = trip.declared_bytes();
let sequence = trip.reply_sequence();
Log::new(Some(status_tx)).warn(format!(
"MySQL connection {connection_id} decision=fail_closed_packet_too_large: {remote_addr} \
declared a {declared}-byte packet, limit is {} bytes; answering 1153 \
ER_NET_PACKET_TOO_LARGE",
packet_limit::MAX_PACKET_BYTES
));
let err = packet_limit::packet_too_large_err(sequence);
if let Err(e) = writer.write_all(&err).await {
debug!("MySQL {connection_id}: could not write the 1153 refusal: {e}");
return;
}
if let Err(e) = writer.flush().await {
debug!("MySQL {connection_id}: could not flush the 1153 refusal: {e}");
}
let mut drained = 0usize;
let mut scratch = vec![0u8; 64 * 1024];
let _ = tokio::time::timeout(LINGER_DRAIN_TIMEOUT, async {
while drained < LINGER_DRAIN_BYTES {
match reader.read(&mut scratch).await {
Ok(0) | Err(_) => break,
Ok(n) => drained = drained.saturating_add(n),
}
}
})
.await;
trace!("MySQL {connection_id}: drained {drained} bytes after refusing the packet");
if let Err(e) = writer.shutdown().await {
debug!("MySQL {connection_id}: shutdown after refusal returned: {e}");
}
}
}
pub struct MysqlHandler {
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
#[allow(dead_code)]
server_id: Option<crate::state::ServerId>,
#[allow(dead_code)]
remote_addr: SocketAddr,
protocol: Arc<MysqlProtocol>,
prepared_statements: Arc<Mutex<std::collections::HashMap<u32, String>>>,
next_stmt_id: Arc<Mutex<u32>>,
fast_auth: Arc<caching_sha2::FastAuthGate>,
}
impl MysqlHandler {
pub fn new(
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
remote_addr: SocketAddr,
) -> Self {
let protocol = Arc::new(MysqlProtocol::new(
connection_id,
app_state.clone(),
status_tx.clone(),
));
Self {
connection_id,
llm_client,
app_state,
status_tx,
server_id,
remote_addr,
protocol,
prepared_statements: Arc::new(Mutex::new(std::collections::HashMap::new())),
next_stmt_id: Arc::new(Mutex::new(1)),
fast_auth: Arc::new(caching_sha2::FastAuthGate::default()),
}
}
pub fn fast_auth_gate(&self) -> Arc<caching_sha2::FastAuthGate> {
self.fast_auth.clone()
}
}
#[async_trait]
impl<W: tokio::io::AsyncWrite + Send + Unpin> AsyncMysqlShim<W> for MysqlHandler {
type Error = io::Error;
fn default_auth_plugin(&self) -> &str {
caching_sha2::CACHING_SHA2_PASSWORD
}
async fn auth_plugin_for_username(&self, _user: &[u8]) -> &str {
""
}
async fn authenticate(
&self,
_auth_plugin: &str,
_username: &[u8],
_salt: &[u8],
auth_data: &[u8],
) -> bool {
if caching_sha2::awaits_fast_auth_success(auth_data) {
self.fast_auth.arm();
}
true
}
async fn on_prepare<'a>(
&'a mut self,
query: &'a str,
info: StatementMetaWriter<'a, W>,
) -> io::Result<()> {
Log::new(Some(&self.status_tx)).debug(format!("MySQL PREPARE: {}", query));
self.record_stats(Some(query.len() as u64), None, Some(1), None)
.await;
let mut next_id = self.next_stmt_id.lock().await;
let stmt_id = *next_id;
*next_id = next_id.wrapping_add(1);
drop(next_id);
let mut stmts = self.prepared_statements.lock().await;
if stmts.len() >= MAX_PREPARED_STATEMENTS {
drop(stmts);
warn!(
"MySQL connection {} exceeded {} prepared statements",
self.connection_id, MAX_PREPARED_STATEMENTS
);
return info
.error(
ErrorKind::ER_MAX_PREPARED_STMT_COUNT_REACHED,
b"Can't create more than max_prepared_stmt_count statements",
)
.await;
}
stmts.insert(stmt_id, query.to_string());
drop(stmts);
let placeholders = count_placeholders(query);
if placeholders > MAX_PLACEHOLDERS {
warn!(
"MySQL connection {}: statement declares {} placeholders, more than the {} \
supported",
self.connection_id, placeholders, MAX_PLACEHOLDERS
);
let mut stmts = self.prepared_statements.lock().await;
stmts.remove(&stmt_id);
drop(stmts);
return info
.error(
ErrorKind::ER_PS_MANY_PARAM,
b"Prepared statement contains too many placeholders",
)
.await;
}
self.record_stats(None, Some(0), None, Some(1)).await;
info.reply(stmt_id, &PLACEHOLDER_COLUMNS[..placeholders], &[])
.await
}
async fn on_execute<'a>(
&'a mut self,
stmt_id: u32,
_params: ParamParser<'a>,
results: QueryResultWriter<'a, W>,
) -> io::Result<()> {
Log::new(Some(&self.status_tx)).debug(format!("MySQL EXECUTE statement {}", stmt_id));
let stmts = self.prepared_statements.lock().await;
let query = stmts.get(&stmt_id).cloned();
drop(stmts);
if let Some(query) = query {
self.handle_query(&query, results).await
} else {
warn!(
"MySQL EXECUTE for unknown or expired statement id {} (decision=unknown_handle)",
stmt_id
);
results
.error(
ErrorKind::ER_UNKNOWN_STMT_HANDLER,
format!(
"Unknown prepared statement handler ({}) given to EXECUTE",
stmt_id
)
.as_bytes(),
)
.await
}
}
async fn on_close(&mut self, stmt_id: u32) {
Log::new(Some(&self.status_tx)).debug(format!("MySQL CLOSE statement {}", stmt_id));
let mut stmts = self.prepared_statements.lock().await;
stmts.remove(&stmt_id);
}
async fn on_query<'a>(
&'a mut self,
query: &'a str,
results: QueryResultWriter<'a, W>,
) -> io::Result<()> {
Log::new(Some(&self.status_tx)).debug(format!("MySQL QUERY: {}", query));
self.handle_query(query, results).await
}
async fn on_init<'a>(
&'a mut self,
_database: &'a str,
writer: InitWriter<'a, W>,
) -> io::Result<()> {
Log::new(Some(&self.status_tx)).debug(format!("MySQL INIT DB: {}", _database));
self.record_stats(Some(_database.len() as u64), None, Some(1), Some(1))
.await;
writer.ok().await
}
}
impl MysqlHandler {
async fn record_stats(
&self,
bytes_in: Option<u64>,
bytes_out: Option<u64>,
packets_in: Option<u64>,
packets_out: Option<u64>,
) {
if let Some(server_id) = self.server_id {
self.app_state
.update_connection_stats(
server_id,
self.connection_id,
bytes_in,
bytes_out,
packets_in,
packets_out,
)
.await;
}
}
async fn handle_query<'a, W: tokio::io::AsyncWrite + Send + Unpin>(
&'a mut self,
query: &str,
results: QueryResultWriter<'a, W>,
) -> io::Result<()> {
trace!("Calling LLM for MySQL query: {}", query);
self.record_stats(Some(query.len() as u64), None, Some(1), None)
.await;
let mut event_data = serde_json::json!({ "query": query });
if let Some(hint) = actions::answer_with_for_query(query) {
event_data["answer_with"] = serde_json::json!(hint);
}
let event = Event::new(&MYSQL_QUERY_EVENT, event_data);
let server_id = self
.server_id
.unwrap_or_else(|| crate::state::ServerId::new(0));
let llm_result = call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(self.connection_id),
&event,
self.protocol.as_ref(),
)
.await;
match llm_result {
Ok(execution_result) => {
let close_requested = execution_result
.protocol_results
.iter()
.any(|r| matches!(r, ActionResult::CloseConnection));
let responses = execution_result
.protocol_results
.iter()
.filter(|r| {
matches!(r, ActionResult::Custom { name, .. }
if matches!(name.as_str(), "mysql_query_response" | "mysql_error" | "mysql_ok"))
})
.count();
if responses > 1 {
warn!(
"MySQL connection {} decision=duplicate_response_dropped: {} response \
actions for one query; sending the first",
self.connection_id, responses
);
}
for result in execution_result.protocol_results {
match result {
ActionResult::Custom { name, data } => {
match name.as_str() {
"mysql_query_response" => {
let columns = data
.get("columns")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let rows = data
.get("rows")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let sent_bytes: u64 = columns
.iter()
.filter_map(|c| c.get("name"))
.filter_map(|v| v.as_str())
.map(|s| s.len() as u64)
.sum::<u64>()
+ rows
.iter()
.filter_map(|r| r.as_array())
.flatten()
.map(|v| json_to_mysql_string(v).len() as u64)
.sum::<u64>();
self.record_stats(None, Some(sent_bytes), None, Some(1))
.await;
return finish_query(
send_result_set(results, columns, rows).await,
close_requested,
);
}
"mysql_error" => {
let error_code = data
.get("error_code")
.and_then(|v| v.as_u64())
.and_then(|c| u16::try_from(c).ok())
.unwrap_or(1064);
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
Log::new(Some(&self.status_tx))
.warn(format!("MySQL error {}: {}", error_code, message));
self.record_stats(
None,
Some(message.len() as u64),
None,
Some(1),
)
.await;
return finish_query(
results
.error(mysql_error_kind(error_code), message.as_bytes())
.await,
close_requested,
);
}
"mysql_ok" => {
let affected_rows = data
.get("affected_rows")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let last_insert_id = data
.get("last_insert_id")
.and_then(|v| v.as_u64())
.unwrap_or(0);
self.record_stats(None, Some(0), None, Some(1)).await;
return finish_query(
results
.completed(OkResponse {
header: 0,
affected_rows,
last_insert_id,
status_flags: StatusFlags::empty(),
warnings: 0,
info: String::new(),
session_state_info: String::new(),
})
.await,
close_requested,
);
}
_ => {
}
}
}
_ => {
}
}
}
warn!(
"MySQL: no response action produced for query {:?} \
(decision=fail_closed_no_action); replying with an ERR packet",
query
);
let message = crate::utils::WireFailure::Unavailable.prefixed_text();
self.record_stats(None, Some(message.len() as u64), None, Some(1))
.await;
finish_query(
results
.error(ErrorKind::ER_UNKNOWN_ERROR, message.as_bytes())
.await,
close_requested,
)
}
Err(e) => {
let overloaded = crate::llm::is_overload_error(&e);
let message = crate::utils::WireFailure::classify(&e).prefixed_text();
let kind = if overloaded {
ErrorKind::ER_LOCK_WAIT_TIMEOUT
} else {
ErrorKind::ER_UNKNOWN_ERROR
};
Log::new(Some(&self.status_tx))
.warn(format!("MySQL replying with error: {}", message));
self.record_stats(None, Some(message.len() as u64), None, Some(1))
.await;
results.error(kind, message.as_bytes()).await
}
}
}
}
fn mysql_error_kind(code: u16) -> ErrorKind {
match code {
1044 => ErrorKind::ER_DBACCESS_DENIED_ERROR,
1045 => ErrorKind::ER_ACCESS_DENIED_ERROR,
1046 => ErrorKind::ER_NO_DB_ERROR,
1049 => ErrorKind::ER_BAD_DB_ERROR,
1050 => ErrorKind::ER_TABLE_EXISTS_ERROR,
1051 => ErrorKind::ER_BAD_TABLE_ERROR,
1052 => ErrorKind::ER_NON_UNIQ_ERROR,
1054 => ErrorKind::ER_BAD_FIELD_ERROR,
1062 => ErrorKind::ER_DUP_ENTRY,
1064 => ErrorKind::ER_PARSE_ERROR,
1065 => ErrorKind::ER_EMPTY_QUERY,
1136 => ErrorKind::ER_WRONG_VALUE_COUNT_ON_ROW,
1146 => ErrorKind::ER_NO_SUCH_TABLE,
1149 => ErrorKind::ER_SYNTAX_ERROR,
1216 => ErrorKind::ER_NO_REFERENCED_ROW,
1217 => ErrorKind::ER_ROW_IS_REFERENCED,
1364 => ErrorKind::ER_NO_DEFAULT_FOR_FIELD,
1451 => ErrorKind::ER_ROW_IS_REFERENCED_2,
1452 => ErrorKind::ER_NO_REFERENCED_ROW_2,
1690 => ErrorKind::ER_DATA_OUT_OF_RANGE,
other => {
warn!(
"MySQL: error code {} is not in opensrv-mysql's table, reporting 1105 ER_UNKNOWN_ERROR",
other
);
ErrorKind::ER_UNKNOWN_ERROR
}
}
}
pub fn count_placeholders(sql: &str) -> usize {
let bytes = sql.as_bytes();
let mut count = 0usize;
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
q @ (b'\'' | b'"' | b'`') => {
let escapes_with_backslash = q != b'`';
i += 1;
while i < bytes.len() {
if escapes_with_backslash && bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == q {
if bytes.get(i + 1) == Some(&q) {
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
}
b'#' => {
i += 1;
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'-' if bytes.get(i + 1) == Some(&b'-')
&& bytes.get(i + 2).is_none_or(|c| c.is_ascii_whitespace()) =>
{
i += 2;
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {
i += 2;
while i < bytes.len() {
if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
i += 2;
break;
}
i += 1;
}
}
b'?' => {
count += 1;
i += 1;
}
_ => i += 1,
}
}
count
}
fn finish_query(result: io::Result<()>, close_requested: bool) -> io::Result<()> {
match result {
Ok(()) if close_requested => Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"MySQL connection closed by close_this_connection action",
)),
other => other,
}
}
async fn send_result_set<'a, W: tokio::io::AsyncWrite + Send + Unpin>(
results: QueryResultWriter<'a, W>,
columns: Vec<serde_json::Value>,
rows: Vec<serde_json::Value>,
) -> io::Result<()> {
let mut cols = Vec::new();
for col_def in &columns {
let name = col_def
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("column");
let col_type = col_def
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("VARCHAR");
let mysql_type = match col_type.to_uppercase().as_str() {
"INT" | "INTEGER" => ColumnType::MYSQL_TYPE_LONG,
"BIGINT" => ColumnType::MYSQL_TYPE_LONGLONG,
"SMALLINT" => ColumnType::MYSQL_TYPE_SHORT,
"TINYINT" => ColumnType::MYSQL_TYPE_TINY,
"FLOAT" => ColumnType::MYSQL_TYPE_FLOAT,
"DOUBLE" => ColumnType::MYSQL_TYPE_DOUBLE,
"DECIMAL" => ColumnType::MYSQL_TYPE_DECIMAL,
"DATE" => ColumnType::MYSQL_TYPE_DATE,
"TIME" => ColumnType::MYSQL_TYPE_TIME,
"DATETIME" | "TIMESTAMP" => ColumnType::MYSQL_TYPE_DATETIME,
"BLOB" | "BINARY" => ColumnType::MYSQL_TYPE_BLOB,
"TEXT" => ColumnType::MYSQL_TYPE_STRING,
_ => ColumnType::MYSQL_TYPE_VAR_STRING,
};
cols.push(Column {
table: "".to_string(),
column: name.to_string(),
coltype: mysql_type,
colflags: ColumnFlags::empty(),
});
}
let mut row_writer = results.start(&cols).await?;
for row_data in &rows {
let Some(row_values) = row_data.as_array() else {
warn!("MySQL: skipping row that is not an array: {}", row_data);
continue;
};
if row_values.len() != cols.len() {
warn!(
"MySQL: row has {} values for {} columns; padding/truncating",
row_values.len(),
cols.len()
);
}
for (idx, col) in cols.iter().enumerate() {
let value = row_values.get(idx).unwrap_or(&serde_json::Value::Null);
write_cell(&mut row_writer, col, value)?;
}
row_writer.end_row().await?;
}
row_writer.finish().await
}
fn write_cell<W: tokio::io::AsyncWrite + Send + Unpin>(
row: &mut RowWriter<'_, W>,
column: &Column,
value: &serde_json::Value,
) -> io::Result<()> {
if value.is_null() {
return row.write_col(None::<String>);
}
macro_rules! unrepresentable {
($row:expr, $column:expr, $value:expr) => {{
warn!(
"MySQL: value {} cannot be sent as column '{}' ({:?}); sending NULL",
$value, $column.column, $column.coltype
);
$row.write_col(None::<String>)
}};
}
match column.coltype {
ColumnType::MYSQL_TYPE_TINY
| ColumnType::MYSQL_TYPE_SHORT
| ColumnType::MYSQL_TYPE_INT24
| ColumnType::MYSQL_TYPE_LONG => {
match json_to_i64(value).and_then(|n| i32::try_from(n).ok()) {
Some(n) => row.write_col(n),
None => unrepresentable!(row, column, value),
}
}
ColumnType::MYSQL_TYPE_LONGLONG => match json_to_i64(value) {
Some(n) => row.write_col(n),
None => unrepresentable!(row, column, value),
},
ColumnType::MYSQL_TYPE_FLOAT => match json_to_f64(value) {
Some(n) => row.write_col(n as f32),
None => unrepresentable!(row, column, value),
},
ColumnType::MYSQL_TYPE_DOUBLE => match json_to_f64(value) {
Some(n) => row.write_col(n),
None => unrepresentable!(row, column, value),
},
ColumnType::MYSQL_TYPE_DATE => match value.as_str().and_then(parse_date) {
Some(d) => row.write_col(d),
None => unrepresentable!(row, column, value),
},
ColumnType::MYSQL_TYPE_DATETIME | ColumnType::MYSQL_TYPE_TIMESTAMP => {
match value.as_str().and_then(parse_datetime) {
Some(dt) => row.write_col(dt),
None => unrepresentable!(row, column, value),
}
}
ColumnType::MYSQL_TYPE_TIME => match value.as_str().and_then(parse_time) {
Some(d) => row.write_col(d),
None => unrepresentable!(row, column, value),
},
_ => row.write_col(json_to_mysql_string(value)),
}
}
fn json_to_i64(value: &serde_json::Value) -> Option<i64> {
match value {
serde_json::Value::Number(n) => n
.as_i64()
.or_else(|| n.as_u64().and_then(|u| i64::try_from(u).ok()))
.or_else(|| {
n.as_f64()
.filter(|f| f.fract() == 0.0 && f.is_finite())
.map(|f| f as i64)
}),
serde_json::Value::String(s) => s.trim().parse::<i64>().ok(),
serde_json::Value::Bool(b) => Some(*b as i64),
_ => None,
}
}
fn json_to_f64(value: &serde_json::Value) -> Option<f64> {
match value {
serde_json::Value::Number(n) => n.as_f64(),
serde_json::Value::String(s) => s.trim().parse::<f64>().ok(),
serde_json::Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
_ => None,
}
}
fn parse_date(s: &str) -> Option<chrono::NaiveDate> {
chrono::NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d").ok()
}
fn parse_datetime(s: &str) -> Option<chrono::NaiveDateTime> {
let s = s.trim();
for format in [
"%Y-%m-%d %H:%M:%S%.f",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S",
] {
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, format) {
return Some(dt);
}
}
parse_date(s).map(|d| d.into())
}
fn parse_time(s: &str) -> Option<std::time::Duration> {
let s = s.trim();
let (whole, fraction) = match s.split_once('.') {
Some((w, f)) => (w, f),
None => (s, ""),
};
let mut parts = whole.split(':');
let hours: u64 = parts.next()?.parse().ok()?;
let minutes: u64 = parts.next()?.parse().ok()?;
let seconds: u64 = parts.next().unwrap_or("0").parse().ok()?;
if parts.next().is_some() || minutes > 59 || seconds > 59 {
return None;
}
let micros: u32 = if fraction.is_empty() {
0
} else {
let padded = format!("{:0<6}", &fraction[..fraction.len().min(6)]);
padded.parse().ok()?
};
let total_secs = hours * 3600 + minutes * 60 + seconds;
if total_secs / (24 * 3600) > 34 {
return None;
}
Some(std::time::Duration::new(total_secs, micros * 1_000))
}
fn json_to_mysql_string(v: &serde_json::Value) -> String {
match v {
serde_json::Value::Null => String::new(),
serde_json::Value::Bool(b) => if *b { "1" } else { "0" }.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => v.to_string(),
}
}