Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 sync and tokio modules 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 API
  • tokio (default): Asynchronous API with Tokio
  • sync-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 handler
  • query_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 execution
  • exec: execute a prepared statement with a handler
  • exec_drop: execute and discard all results
  • exec_first: execute and return Option<Row> for the first row
  • exec_collect: execute and collect all rows into a Vec
  • exec_foreach: execute and call a closure for each row
  • exec_bulk_insert_or_update: uses MariaDB’s COM_STMT_BULK_EXECUTE to send all parameters in a single packet; falls back to multiple exec() 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 TypeMySQL TypeNotes
boolTINYINTEncoded as 0 or 1
i8TINYINT
i16SMALLINT
i32INT
i64BIGINT
u8TINYINT UNSIGNED
u16SMALLINT UNSIGNED
u32INT UNSIGNED
u64BIGINT UNSIGNED
f32FLOAT
f64DOUBLE
&strVARCHAR
StringVARCHAR
&[u8]BLOB
Vec<u8>BLOB
Option<T>Same as TNone 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 TypeRust Types
TINYINTi8, i16, i32, i64, bool
SMALLINTi16, i32, i64
MEDIUMINT, INTi32, i64
BIGINTi64
TINYINT UNSIGNEDu8, u16, u32, u64, bool
SMALLINT UNSIGNEDu16, u32, u64
MEDIUMINT UNSIGNED, INT UNSIGNEDu32, u64
BIGINT UNSIGNEDu64
FLOATf32, f64
DOUBLEf64
VARCHAR, CHAR, TEXT, etc.&str, String
BLOB, BINARY, VARBINARY, etc.&[u8], Vec<u8>
NULLOption<T>

Date and Time Types

Date/time types are exposed through the Value enum:

MySQL TypeValue Variants
DATEDate0, Date4
DATETIME, TIMESTAMPDatetime0, Datetime4, Datetime7, Datetime11
TIMETime0, 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 TypeMySQL TypeNotes
uuid::UuidVARCHAR(36)Encoded as hyphenated string
MySQL TypeRust TypeNotes
VARCHAR, CHARuuid::UuidParsed from hyphenated string
BINARY(16)uuid::UuidParsed from 16 bytes

with-chrono (chrono crate)

Rust TypeMySQL Type
chrono::NaiveDateDATE
chrono::NaiveTimeTIME
chrono::NaiveDateTimeDATETIME
MySQL TypeRust TypeNotes
DATEchrono::NaiveDateZero dates (0000-00-00) return an error
TIMEchrono::NaiveTimeNegative times or times with days return an error
DATETIME, TIMESTAMPchrono::NaiveDateTimeZero datetimes return an error

with-time (time crate)

Rust TypeMySQL Type
time::DateDATE
time::TimeTIME
time::PrimitiveDateTimeDATETIME
MySQL TypeRust TypeNotes
DATEtime::DateZero dates return an error
TIMEtime::TimeNegative times or times with days return an error
DATETIME, TIMESTAMPtime::PrimitiveDateTimeZero datetimes return an error

with-rust-decimal (rust_decimal crate)

Rust TypeMySQL Type
rust_decimal::DecimalDECIMAL
MySQL TypeRust TypeNotes
DECIMALrust_decimal::Decimal96-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 issues
  • DEBUG: Query execution details
  • TRACE: 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:

  1. Derive RefFromRow
  2. Have #[repr(C, packed)] attribute
  3. 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 TypeWire SizeDescription
i8 / u81 byteSingle-byte (endian-agnostic)
I16LE / U16LE2 bytes16-bit little-endian
I32LE / U32LE4 bytes32-bit little-endian
I64LE / U64LE8 bytes64-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. Use FromRow for nullable columns.
  • Fixed-size types only: Variable-length types like VARCHAR, TEXT, BLOB are 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

FeatureFromRowRefFromRow
AllocationYes (per row)No
NULL supportYes (Option<T>)No
Variable-length typesYesNo
Column name matchingYesNo
Return typeOwned TReference &T
APIexec_first, exec_collect, exec_foreachexec_foreach_ref

How It Works

  1. The derive macro generates zerocopy trait implementations (FromBytes, KnownLayout, Immutable)
  2. At compile time, it verifies all fields implement FixedWireSize
  3. At runtime, the row buffer is cast directly to &YourStruct using zerocopy
  4. No parsing, no allocation - just a pointer cast with size validation