Introduction
zero-mysql is a high-performance MySQL client library for Rust.
[dependencies]
zero-mysql = "*"
Requires Rust nightly.
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(())
})?;
Features
- Zero-Copy: Minimal allocations and copies in hot paths
- Zero-Allocation: Reuse buffers across queries
- Sync and Async: Both
syncandtokiomodules available - Binary Protocol: Prepared statements with automatic caching
- MariaDB Bulk Execution: Single round-trip bulk operations
- Customizable Deserialization: Process rows without intermediate allocations
Feature Flags
sync(default): Synchronous APItokio(default): Asynchronous API with Tokiosync-tls: TLS support for synchronous API (experimental)tokio-tls: TLS support for asynchronous API (experimental)
Limitations
- No Streaming: All results are fetched into memory
- Nightly Rust Required: Uses unstable features for performance
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
For maximum performance, zero-mysql provides zero-copy row decoding through the RefFromRow trait. This allows you to decode rows as references directly into the read buffer, avoiding any memory allocation or copying.
When to Use
Zero-copy decoding is useful when:
- Processing large result sets where allocation overhead matters
- All columns are fixed-size types (integers, floats)
- All columns are
NOT NULL - You don’t need to store the decoded rows (processing in a callback)
Requirements
To use zero-copy decoding, your struct must:
- Derive
RefFromRow - Have
#[repr(C, packed)]attribute - Use little-endian types from
zerocopy(MySQL uses little-endian wire format)
Example
#![allow(unused)]
fn main() {
use zero_mysql::ref_row::{RefFromRow, I64LE, I32LE};
use zero_mysql_derive::RefFromRow;
#[derive(RefFromRow)]
#[repr(C, packed)]
struct UserStats {
user_id: I64LE, // 8 bytes
login_count: I32LE, // 4 bytes
}
// Process rows without allocation
conn.exec_foreach_ref::<UserStats, _, _>(&mut stmt, (), |row| {
// row is &UserStats - a reference into the buffer
println!("user_id: {}", row.user_id.get());
println!("login_count: {}", row.login_count.get());
Ok(())
})?;
}
Available Types
MySQL uses little-endian encoding on the wire. Use these types:
| Rust Type | Wire Size | Description |
|---|---|---|
i8 / u8 | 1 byte | Single-byte (endian-agnostic) |
I16LE / U16LE | 2 bytes | 16-bit little-endian |
I32LE / U32LE | 4 bytes | 32-bit little-endian |
I64LE / U64LE | 8 bytes | 64-bit little-endian |
These are re-exported from zero_mysql::ref_row for convenience.
Accessing Values
The endian-aware types provide a .get() method to convert to native integers:
#![allow(unused)]
fn main() {
let user_id: i64 = row.user_id.get();
let count: i32 = row.login_count.get();
}
On little-endian platforms (x86, ARM), .get() is a no-op and compiles to zero instructions.
Limitations
- No NULL support: All columns must be
NOT NULL. UseFromRowfor nullable columns. - Fixed-size types only: Variable-length types like
VARCHAR,TEXT,BLOBare not supported. - No column name matching: Columns must match struct field order exactly.
- Callback-based only: Returns references into the buffer, so can only be used with
exec_foreach_ref.
Comparison with FromRow
| Feature | FromRow | RefFromRow |
|---|---|---|
| Allocation | Yes (per row) | No |
| NULL support | Yes (Option<T>) | No |
| Variable-length types | Yes | No |
| Column name matching | Yes | No |
| Return type | Owned T | Reference &T |
| API | exec_first, exec_collect, exec_foreach | exec_foreach_ref |
How It Works
- The derive macro generates
zerocopytrait implementations (FromBytes,KnownLayout,Immutable) - At compile time, it verifies all fields implement
FixedWireSize - At runtime, the row buffer is cast directly to
&YourStructusing zerocopy - No parsing, no allocation - just a pointer cast with size validation