Introduction
zero-mysql is a high-performance MySQL client library for Rust.
[dependencies]
zero-mysql = "*"
See Feature Flags in the README.
Quick Start
use zero_mysql::sync::Conn;
let mut conn = Conn::new("mysql://user:password@localhost/mydb")?;
// Text protocol query
conn.query_drop("INSERT INTO users (name) VALUES ('Alice')")?;
// Prepared statement
let mut stmt = conn.prepare("SELECT * FROM users WHERE id = ?")?;
conn.exec_drop(&mut stmt, (42,))?;
// Transaction
conn.transaction(|conn, _tx| {
conn.query_drop("INSERT INTO users (name) VALUES ('Bob')")?;
Ok(())
})?;
Connection
A connection can be made with a URL string or Opts.
The URL format is:
mysql://[user[:password]@]host[:port][/database][?parameters]
Example: Basic
use zero_mysql::sync::Conn;
use zero_mysql::Opts;
// URL
let mut conn = Conn::new("mysql://test:1234@localhost:3306/test_db")?;
// Opts struct
let mut opts = Opts::default();
opts.host = "localhost".to_string();
opts.port = 3306;
opts.user = "test".to_string();
opts.password = "1234".to_string();
opts.db = Some("test_db".to_string());
let mut conn = Conn::new(opts)?;
Example: Async
use zero_mysql::tokio::Conn;
let mut conn = Conn::new("mysql://test:1234@localhost:3306/test_db").await?;
Example: Unix Socket
use zero_mysql::Opts;
use zero_mysql::sync::Conn;
let mut opts = Opts::default();
opts.socket = Some("/var/run/mysqld/mysqld.sock".to_string());
opts.db = Some("test".to_string());
let mut conn = Conn::new(opts)?;
Connection Options
See Opts for all available connection options and URL query parameters.
Upgrade to Unix Socket
By default, upgrade_to_unix_socket is true.
If the connection is made via TCP to localhost, the driver queries SELECT @@socket to get the Unix socket path, then reconnects using the socket for better performance.
For production, disable this flag and manually specify the socket address:
let mut opts = Opts::try_from("mysql://test:1234@localhost")?;
opts.upgrade_to_unix_socket = false;
let mut conn = Conn::new(opts)?;
Query
There are two sets of query APIs: Text Protocol and Binary Protocol.
Text Protocol
Text protocol is simple and supports multiple statements separated by ;, but does not support parameter binding.
Use binary protocol if you need to send parameters or read typed results.
impl Conn {
fn query<H: TextResultSetHandler>(&mut self, sql: &str, handler: &mut H) -> Result<()>;
fn query_drop(&mut self, sql: &str) -> Result<()>;
}
query: executes SQL and processes results with a handlerquery_drop: executes SQL and discards the result
Example
conn.query_drop("INSERT INTO users (name) VALUES ('Alice')")?;
conn.query_drop("DELETE FROM users WHERE id = 1")?;
Binary Protocol
Binary protocol uses prepared statements with parameter binding. Use ? as the placeholder.
impl Conn {
fn prepare(&mut self, sql: &str) -> Result<PreparedStatement>;
fn exec<P, H>(&mut self, stmt: &mut PreparedStatement, params: P, handler: &mut H) -> Result<()>;
fn exec_drop<P>(&mut self, stmt: &mut PreparedStatement, params: P) -> Result<()>;
fn exec_first<Row, P>(&mut self, stmt: &mut PreparedStatement, params: P) -> Result<Option<Row>>;
fn exec_collect<Row, P>(&mut self, stmt: &mut PreparedStatement, params: P) -> Result<Vec<Row>>;
fn exec_foreach<Row, P, F>(&mut self, stmt: &mut PreparedStatement, params: P, f: F) -> Result<()>;
fn exec_bulk_insert_or_update<P, I, H>(...) -> Result<()>;
}
prepare: prepare a statement for executionexec: execute a prepared statement with a handlerexec_drop: execute and discard all resultsexec_first: execute and returnOption<Row>for the first rowexec_collect: execute and collect all rows into a Vecexec_foreach: execute and call a closure for each rowexec_bulk_insert_or_update: uses MariaDB’s COM_STMT_BULK_EXECUTE to send all parameters in a single packet; falls back to multipleexec()calls on Oracle MySQL). This is a huge speedup.
Example: Basic
// Prepare a statement
let mut stmt = conn.prepare("SELECT * FROM users WHERE id = ?")?;
// Execute with parameters
conn.exec_drop(&mut stmt, (42,))?;
// Execute with different parameters (statement is reused)
conn.exec_drop(&mut stmt, (100,))?;
Example: Bulk Execution
On MariaDB, bulk execution sends all parameters in a single packet using the bulk command extension. On Oracle MySQL, it falls back to multiple exec() calls:
use zero_mysql::protocol::command::bulk_exec::BulkFlags;
let mut stmt = conn.prepare("INSERT INTO users (age, name) VALUES (?, ?)")?;
conn.exec_bulk_insert_or_update(
&mut stmt,
vec![
(20, "Alice"),
(21, "Bob"),
(22, "Charlie"),
],
BulkFlags::empty(),
&mut handler,
)?;
Statement Caching
Prepared statements are cached per connection. After calling prepare(), reuse the PreparedStatement for subsequent executions.
// Prepare once
let mut stmt = conn.prepare("SELECT * FROM users WHERE id = ?")?;
// Reuse multiple times
conn.exec_drop(&mut stmt, (1,))?;
conn.exec_drop(&mut stmt, (2,))?;
conn.exec_drop(&mut stmt, (3,))?;
Struct Mapping
There are two ways to map database rows to Rust structs.
Using #[derive(FromRow)]
The FromRow derive macro automatically maps columns to struct fields by name.
use zero_mysql::r#macro::FromRow;
#[derive(FromRow)]
struct User {
id: i64,
name: String,
email: Option<String>,
}
let mut stmt = conn.prepare("SELECT id, name, email FROM users")?;
// Collect all rows
let users: Vec<User> = conn.exec_collect(&mut stmt, ())?;
// Get first row only
let user: Option<User> = conn.exec_first(&mut stmt, ())?;
// Process rows one by one
conn.exec_foreach(&mut stmt, (), |user: User| {
println!("{}: {}", user.id, user.name);
Ok(())
})?;
Use #[from_row(strict)] to error on unknown columns:
#[derive(FromRow)]
#[from_row(strict)]
struct User {
id: i64,
name: String,
}
// Errors if query returns columns other than `id` and `name`
Manual Construction with exec_foreach
For custom logic or computed fields:
struct User {
id: i64,
name: String,
display_name: String, // computed field
}
let mut stmt = conn.prepare("SELECT id, name FROM users")?;
let mut users = Vec::new();
conn.exec_foreach(&mut stmt, (), |row: (i64, String)| {
users.push(User {
id: row.0,
display_name: format!("User: {}", row.1),
name: row.1,
});
Ok(())
})?;
Data Type
The library intentionally rejects conversions that could silently lose data. For example, reading a BIGINT column as i8 will return an error rather than truncating the value. This ensures data integrity and makes bugs easier to catch.
Widening conversions (e.g., reading TINYINT as i64) are allowed.
Parameter Types (Rust to MySQL)
| Rust Type | MySQL Type | Notes |
|---|---|---|
bool | TINYINT | Encoded as 0 or 1 |
i8 | TINYINT | |
i16 | SMALLINT | |
i32 | INT | |
i64 | BIGINT | |
u8 | TINYINT UNSIGNED | |
u16 | SMALLINT UNSIGNED | |
u32 | INT UNSIGNED | |
u64 | BIGINT UNSIGNED | |
f32 | FLOAT | |
f64 | DOUBLE | |
&str | VARCHAR | |
String | VARCHAR | |
&[u8] | BLOB | |
Vec<u8> | BLOB | |
Option<T> | Same as T | None encodes as NULL |
Result Types (MySQL to Rust)
Signed and unsigned types are strictly separated. You cannot decode a signed column (e.g., TINYINT) to an unsigned Rust type (e.g., u8), or vice versa.
| MySQL Type | Rust Types |
|---|---|
TINYINT | i8, i16, i32, i64, bool |
SMALLINT | i16, i32, i64 |
MEDIUMINT, INT | i32, i64 |
BIGINT | i64 |
TINYINT UNSIGNED | u8, u16, u32, u64, bool |
SMALLINT UNSIGNED | u16, u32, u64 |
MEDIUMINT UNSIGNED, INT UNSIGNED | u32, u64 |
BIGINT UNSIGNED | u64 |
FLOAT | f32, f64 |
DOUBLE | f64 |
VARCHAR, CHAR, TEXT, etc. | &str, String |
BLOB, BINARY, VARBINARY, etc. | &[u8], Vec<u8> |
NULL | Option<T> |
Date and Time Types
Date/time types are exposed through the Value enum:
| MySQL Type | Value Variants |
|---|---|
DATE | Date0, Date4 |
DATETIME, TIMESTAMP | Datetime0, Datetime4, Datetime7, Datetime11 |
TIME | Time0, Time8, Time12 |
The numeric suffix indicates the wire format byte length.
DECIMAL Type
DECIMAL and NUMERIC columns are returned as Value::Byte containing the string representation.
Feature-Gated Types
Additional type support is available through feature flags.
with-uuid (uuid crate)
| Rust Type | MySQL Type | Notes |
|---|---|---|
uuid::Uuid | VARCHAR(36) | Encoded as hyphenated string |
| MySQL Type | Rust Type | Notes |
|---|---|---|
VARCHAR, CHAR | uuid::Uuid | Parsed from hyphenated string |
BINARY(16) | uuid::Uuid | Parsed from 16 bytes |
with-chrono (chrono crate)
| Rust Type | MySQL Type |
|---|---|
chrono::NaiveDate | DATE |
chrono::NaiveTime | TIME |
chrono::NaiveDateTime | DATETIME |
| MySQL Type | Rust Type | Notes |
|---|---|---|
DATE | chrono::NaiveDate | Zero dates (0000-00-00) return an error |
TIME | chrono::NaiveTime | Negative times or times with days return an error |
DATETIME, TIMESTAMP | chrono::NaiveDateTime | Zero datetimes return an error |
with-time (time crate)
| Rust Type | MySQL Type |
|---|---|
time::Date | DATE |
time::Time | TIME |
time::PrimitiveDateTime | DATETIME |
| MySQL Type | Rust Type | Notes |
|---|---|---|
DATE | time::Date | Zero dates return an error |
TIME | time::Time | Negative times or times with days return an error |
DATETIME, TIMESTAMP | time::PrimitiveDateTime | Zero datetimes return an error |
with-rust-decimal (rust_decimal crate)
| Rust Type | MySQL Type |
|---|---|
rust_decimal::Decimal | DECIMAL |
| MySQL Type | Rust Type | Notes |
|---|---|---|
DECIMAL | rust_decimal::Decimal | 96-bit precision, not arbitrary like MySQL |
Transaction
Transactions ensure a group of operations either all succeed (commit) or all fail (rollback).
Using Transactions
use zero_mysql::sync::Conn;
conn.transaction(|conn, _tx| {
conn.query_drop("INSERT INTO users (name) VALUES ('Alice')")?;
conn.query_drop("INSERT INTO users (name) VALUES ('Bob')")?;
Ok(())
})?;
If the closure returns Ok, the transaction is automatically committed. If the closure returns Err, the transaction is automatically rolled back.
Automatic Rollback on Error
conn.transaction(|conn, _tx| {
conn.query_drop("INSERT INTO users (name) VALUES ('Alice')")?;
// Returns error - transaction will be rolled back
Err(Error::BadUsageError("oops".to_string()))
})?;
// No data inserted
Explicit Commit/Rollback
Use tx.commit() or tx.rollback() for explicit control:
conn.transaction(|conn, tx| {
conn.query_drop("INSERT INTO users (name) VALUES ('Alice')")?;
if some_condition {
tx.commit(conn)
} else {
tx.rollback(conn)
}
})?;
Nested Transactions
Nested transactions are not supported. Calling transaction while already in a transaction returns Error::NestedTransaction.
Async Transactions
For async connections, use async closures:
use zero_mysql::tokio::Conn;
conn.transaction(async |conn, _tx| {
conn.query_drop("INSERT INTO users (name) VALUES ('Alice')").await?;
Ok(())
}).await?;
Logging
zero-mysql uses the tracing crate for logging and instrumentation.
Setup
Add tracing-subscriber to your dependencies:
[dependencies]
tracing-subscriber = "0.3"
Initialize the subscriber:
tracing_subscriber::fmt::init();
Log Levels
WARN: Connection errors and protocol issuesDEBUG: Query execution detailsTRACE: Low-level protocol packets
Example
use tracing_subscriber;
fn main() {
// Initialize logging
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
let mut conn = Conn::new("mysql://localhost")?;
conn.query_drop("SELECT 1")?; // Will log query execution
}
Performance Note
In release builds, tracing macros above WARN level are compiled out via the release_max_level_warn feature for minimal runtime overhead.
Zero-Copy Decoding
The feature is available. TODO: docs