feat(mysql): native SQL execution — fix #41 type decoding, in-process restore, .sql open-with (#42)

Fixes #41.

MySQL columns now decode by their declared type instead of a blind try_get chain,
so every non-string type (integers, DECIMAL, BIT, YEAR, DATE/TIME/DATETIME/TIMESTAMP)
renders its value instead of NULL. MySQL restore runs in-process over the sqlx driver
(no bundled MariaDB client, so MySQL 8 caching_sha2_password works) and the Clean toggle
is implemented. .sql files can be opened with Gridline from the OS file manager.

Verified: cargo test --lib 442 passed / 0 failed; bun run test 133 files / 1151 tests
exit 0; tsc clean; both live MySQL 8 integration tests pass with the bundled CLI hidden.
This commit is contained in:
2026-09-29 08:52:47 +08:00
committed by GitHub
parent ce4a73b0bb
commit 88e7e45818
36 changed files with 2265 additions and 141 deletions
+57
View File
@@ -1,6 +1,7 @@
//! Per-connection cancellation handles, stored independently of the pool
//! lock so `cancel_query` can dispatch while a long query holds the pool mutex.
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use rusqlite::InterruptHandle;
@@ -59,6 +60,12 @@ pub enum CancelHandle {
#[derive(Default)]
pub struct CancelRegistry {
map: Mutex<HashMap<String, CancelHandle>>,
/// Live "cancel requested" flags, keyed by connection id. `cancel_query`
/// sets one for MySQL; long-running in-process operations (currently the
/// native restore engine) poll it between statements and clear it when
/// they finish. Kept in a separate map from the handles so it needs no
/// `Clone` on the registry and survives handle replacement.
flags: Mutex<HashMap<String, Arc<AtomicBool>>>,
}
impl CancelRegistry {
@@ -66,6 +73,41 @@ impl CancelRegistry {
Self::default()
}
/// Get (or create) the cancellation flag for a connection. Polling callers
/// should clear it before starting and after finishing.
pub fn cancel_flag(&self, id: &str) -> Arc<AtomicBool> {
self.flags
.lock()
.unwrap()
.entry(id.to_string())
.or_default()
.clone()
}
/// Mark a connection as cancel-requested. Called by `cancel_query` before
/// it sends the driver-specific cancel so in-process consumers observe the
/// request even when the driver surfaces it as an unrecognized error.
pub fn mark_cancelled(&self, id: &str) {
self.cancel_flag(id).store(true, Ordering::SeqCst);
}
/// Whether a cancel has been requested for this connection.
pub fn is_cancelled(&self, id: &str) -> bool {
self.flags
.lock()
.unwrap()
.get(id)
.map(|f| f.load(Ordering::SeqCst))
.unwrap_or(false)
}
/// Clear the cancel-requested flag for this connection.
pub fn clear_cancelled(&self, id: &str) {
if let Some(f) = self.flags.lock().unwrap().get(id) {
f.store(false, Ordering::SeqCst);
}
}
pub fn set_pg(&self, id: &str, c: PgCancel) {
self.set(id, CancelHandle::Pg(c));
}
@@ -157,6 +199,21 @@ mod tests {
}
}
#[test]
fn cancel_flag_is_tracked_per_connection() {
let reg = CancelRegistry::new();
assert!(!reg.is_cancelled("c1"));
reg.mark_cancelled("c1");
assert!(reg.is_cancelled("c1"));
assert!(!reg.is_cancelled("c2"), "flag must not leak across connections");
reg.clear_cancelled("c1");
assert!(!reg.is_cancelled("c1"));
// The same Arc is shared between getter and setter.
let flag = reg.cancel_flag("c1");
reg.mark_cancelled("c1");
assert!(flag.load(Ordering::SeqCst));
}
fn fake_opts() -> sqlx::mysql::MySqlConnectOptions {
sqlx::mysql::MySqlConnectOptions::new()
.host("127.0.0.1").port(1).username("u").password("p").database("d")