feat(usable): package gui-host validation snapshot
Some checks failed
release-smoke / macos-13 / x86_64-apple-darwin (push) Has been cancelled
release-smoke / ubuntu-latest / x86_64-unknown-linux-gnu (push) Has been cancelled
release-smoke / windows-latest / x86_64-pc-windows-msvc (push) Has been cancelled

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip CTO
2026-03-31 10:21:36 +00:00
parent 19aeb7784b
commit a28dab4cd9
47 changed files with 6894 additions and 771 deletions

View File

@@ -420,7 +420,10 @@ pub fn inspect(
})
}
pub fn query(profile: &ConnectionProfile, request: &QueryRequest) -> Result<QueryResponse, AppError> {
pub fn query(
profile: &ConnectionProfile,
request: &QueryRequest,
) -> Result<QueryResponse, AppError> {
validate_profile("query", profile)?;
request
.validate()

View File

@@ -15,6 +15,7 @@ use std::path::Path;
use std::{error::Error, fmt};
const SQLITE_DATABASE_HEADER: &[u8; 16] = b"SQLite format 3\0";
const MYSQL_UTF8MB4_SESSION_INIT: &str = "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci";
pub trait DatabaseDriver {
fn kind(&self) -> DatabaseKind;
@@ -430,17 +431,38 @@ fn connect_mysql(target: &ConnectionTarget) -> Result<MySqlConn, DriverError> {
.as_deref()
.ok_or_else(|| DriverError::Connection("mysql requires a database".to_string()))?;
let builder = MySqlOptsBuilder::new()
.ip_or_hostname(Some(host))
.tcp_port(port)
.user(target.username.as_deref())
.pass(target.password.as_deref())
.db_name(Some(database))
.prefer_socket(false);
let builder = mysql_opts_builder(
host,
port,
target.username.as_deref(),
target.password.as_deref(),
database,
);
MySqlConn::new(builder).map_err(map_mysql_error)
}
fn mysql_opts_builder(
host: &str,
port: u16,
username: Option<&str>,
password: Option<&str>,
database: &str,
) -> MySqlOptsBuilder {
MySqlOptsBuilder::new()
.ip_or_hostname(Some(host))
.tcp_port(port)
.user(username)
.pass(password)
.db_name(Some(database))
.prefer_socket(false)
.init(mysql_session_init_commands())
}
fn mysql_session_init_commands() -> Vec<&'static str> {
vec![MYSQL_UTF8MB4_SESSION_INIT]
}
fn connect_sqlite(target: &ConnectionTarget) -> Result<SqliteConnection, DriverError> {
let path = match &target.transport {
ConnectionTransport::File { path } => path,
@@ -636,7 +658,7 @@ fn consume_mysql_query_result<T: MySqlProtocol>(
let mut last_rows = Vec::new();
let mut last_rows_affected = 0;
while let Some(result_set) = result.iter() {
while let Some(mut result_set) = result.iter() {
let columns = result_set
.columns()
.as_ref()
@@ -645,18 +667,17 @@ fn consume_mysql_query_result<T: MySqlProtocol>(
.collect::<Vec<_>>();
let rows_affected = result_set.affected_rows();
if columns.is_empty() {
last_rows_affected = rows_affected;
continue;
}
let mut rows = Vec::new();
for row in result_set {
while let Some(row) = result_set.next() {
rows.push(mysql_row_to_strings(row.map_err(map_mysql_error)?));
}
last_columns = columns;
last_rows = rows;
if columns.is_empty() {
last_rows_affected = rows_affected;
} else {
last_columns = columns;
last_rows = rows;
}
}
if last_columns.is_empty() {
@@ -925,6 +946,24 @@ mod tests {
assert_eq!(error.to_string(), "mysql authentication failed");
}
#[test]
fn mysql_connection_enforces_utf8mb4_session() {
let opts: mysql::Opts =
mysql_opts_builder("127.0.0.1", 3306, Some("dbtool"), Some("secret"), "qa_demo").into();
assert_eq!(
opts.get_init(),
vec![String::from(MYSQL_UTF8MB4_SESSION_INIT)]
);
}
#[test]
fn mysql_string_values_preserve_multilingual_utf8() {
let rendered = mysql_value_to_string(&MySqlValue::Bytes("张敏😀".as_bytes().to_vec()));
assert_eq!(rendered, "张敏😀");
}
#[test]
fn sqlite_driver_supports_connect_inspect_and_query() {
let unique = SystemTime::now()