ethereum-mysql 4.0.0

Ethereum types (Address, U256) wrapper for seamless SQL database integration with SQLx
Documentation
//! This module is only available when the `sqlx` feature is enabled.
//! Support for the [`sqlx`](https://crates.io/crates/sqlx) crate.
//!
//! This implementation encodes and decodes Ethereum types to and from **binary format**
//! (big-endian bytes) for optimal storage efficiency and performance.
//!
//! Following the same approach as [alloy-rs/core PR #970](https://github.com/alloy-rs/core/pull/970)
//! and [PR #1020](https://github.com/alloy-rs/core/pull/1020), all types use `Vec<u8>` as the
//! underlying SQL type, mapping to database BINARY/BLOB columns.
//!
//! ## Recommended Database Column Types
//!
//! | Type               | MySQL          | PostgreSQL | SQLite  |
//! |--------------------|----------------|-----------|---------|
//! | SqlAddress         | BINARY(20)     | BYTEA     | BLOB    |
//! | SqlU128            | BINARY(16)     | BYTEA     | BLOB    |
//! | SqlU256            | BINARY(32)     | BYTEA     | BLOB    |
//! | SqlFixedBytes\<N\> | BINARY(N)      | BYTEA     | BLOB    |
//! | SqlBytes           | VARBINARY(N)   | BYTEA     | BLOB    |
//!
//! **Note:** This is a breaking change from v3.x which used VARCHAR/TEXT (hex string) storage.
//! See the [CHANGELOG](https://github.com/Rollp0x/ethereum-mysql/blob/main/CHANGELOG.md) for migration guidance.
//!
#![cfg_attr(docsrs, doc(cfg(feature = "sqlx")))]

use thiserror::Error;

use alloy::primitives::Uint;
use sqlx_core::{
    database::Database,
    decode::Decode,
    encode::{Encode, IsNull},
    error::BoxDynError,
    types::Type,
};

/// Error type for decoding failures when converting database values to Ethereum types.
///
/// This is used when a value from the database cannot be represented in the target type,
/// such as when the byte length is incorrect or the bytes cannot be parsed.
#[derive(Error, Debug)]
pub enum DecodeError {
    /// Returned when the database binary value has an incorrect length for an address (expected 20 bytes).
    #[error("Address decode error: expected 20 bytes, got {0} bytes")]
    AddressDecodeError(usize),

    /// Returned when the database binary value cannot be decoded into a Uint.
    #[error("Uint decode error: {0}")]
    UintDecodeError(String),

    /// Returned when the database binary value has an incorrect length for FixedBytes.
    #[error("FixedBytes decode error: expected {expected} bytes, got {actual} bytes")]
    FixedBytesDecodeError {
        /// Expected number of bytes.
        expected: usize,
        /// Actual number of bytes received.
        actual: usize,
    },

    /// Returned when the database binary value cannot be decoded into Bytes.
    #[error("Bytes decode error: {0}")]
    BytesDecodeError(String),
}

use crate::{SqlAddress, SqlBytes, SqlFixedBytes, SqlUint};

// ============================================================================
// SqlAddress — BINARY(20), big-endian
// ============================================================================

impl<DB: Database> Type<DB> for SqlAddress
where
    Vec<u8>: Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <Vec<u8> as Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <Vec<u8> as Type<DB>>::compatible(ty)
    }
}

impl<'a, DB: Database> Encode<'a, DB> for SqlAddress
where
    Vec<u8>: Encode<'a, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
    ) -> Result<IsNull, BoxDynError> {
        // Address is 20 bytes, stored directly as big-endian bytes
        self.0.to_vec().encode_by_ref(buf)
    }
}

impl<'a, DB: Database> Decode<'a, DB> for SqlAddress
where
    Vec<u8>: Decode<'a, DB>,
{
    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
        let bytes = Vec::<u8>::decode(value)?;
        if bytes.len() != 20 {
            return Err(DecodeError::AddressDecodeError(bytes.len()).into());
        }
        let mut arr = [0u8; 20];
        arr.copy_from_slice(&bytes);
        Ok(SqlAddress::new(arr))
    }
}

// ============================================================================
// SqlUint<BITS, LIMBS> — BINARY(BITS/8), big-endian
// ============================================================================

/// Helper: convert a Uint to big-endian bytes (Vec<u8> of length BITS/8).
fn uint_to_be_bytes_vec<const BITS: usize, const LIMBS: usize>(
    value: &Uint<BITS, LIMBS>,
) -> Vec<u8> {
    let bytes_len = BITS / 8;
    let mut bytes = vec![0u8; bytes_len];
    let limbs = value.as_limbs();
    for i in 0..LIMBS {
        if i * 8 >= bytes_len {
            break;
        }
        let src_idx = LIMBS - 1 - i; // most significant limb first
        let limb_bytes = limbs[src_idx].to_be_bytes();
        let dst_start = i * 8;
        let len = 8.min(bytes_len - dst_start);
        bytes[dst_start..dst_start + len].copy_from_slice(&limb_bytes[8 - len..]);
    }
    bytes
}

impl<const BITS: usize, const LIMBS: usize, DB: Database> Type<DB> for SqlUint<BITS, LIMBS>
where
    Vec<u8>: Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <Vec<u8> as Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <Vec<u8> as Type<DB>>::compatible(ty)
    }
}

impl<'a, const BITS: usize, const LIMBS: usize, DB: Database> Encode<'a, DB>
    for SqlUint<BITS, LIMBS>
where
    Vec<u8>: Encode<'a, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
    ) -> Result<IsNull, BoxDynError> {
        uint_to_be_bytes_vec(&self.0).encode_by_ref(buf)
    }
}

impl<'a, const BITS: usize, const LIMBS: usize, DB: Database> Decode<'a, DB>
    for SqlUint<BITS, LIMBS>
where
    Vec<u8>: Decode<'a, DB>,
{
    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
        let bytes = Vec::<u8>::decode(value)?;
        let expected = BITS / 8;
        if bytes.len() != expected {
            return Err(DecodeError::UintDecodeError(format!(
                "expected {expected} bytes, got {}",
                bytes.len()
            ))
            .into());
        }
        Ok(SqlUint(Uint::<BITS, LIMBS>::from_be_slice(&bytes)))
    }
}

// ============================================================================
// SqlFixedBytes<N> — BINARY(N), big-endian
// ============================================================================

impl<const N: usize, DB: Database> Type<DB> for SqlFixedBytes<N>
where
    Vec<u8>: Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <Vec<u8> as Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <Vec<u8> as Type<DB>>::compatible(ty)
    }
}

impl<'a, const N: usize, DB: Database> Encode<'a, DB> for SqlFixedBytes<N>
where
    Vec<u8>: Encode<'a, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
    ) -> Result<IsNull, BoxDynError> {
        self.0.to_vec().encode_by_ref(buf)
    }
}

impl<'a, const N: usize, DB: Database> Decode<'a, DB> for SqlFixedBytes<N>
where
    Vec<u8>: Decode<'a, DB>,
{
    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
        let bytes = Vec::<u8>::decode(value)?;
        if bytes.len() != N {
            return Err(DecodeError::FixedBytesDecodeError {
                expected: N,
                actual: bytes.len(),
            }
            .into());
        }
        let mut arr = [0u8; N];
        arr.copy_from_slice(&bytes);
        Ok(SqlFixedBytes::new(arr))
    }
}

// ============================================================================
// SqlBytes — VARBINARY / BYTEA / BLOB, variable-length binary
// ============================================================================

impl<DB: Database> Type<DB> for SqlBytes
where
    Vec<u8>: Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <Vec<u8> as Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <Vec<u8> as Type<DB>>::compatible(ty)
    }
}

impl<'a, DB: Database> Encode<'a, DB> for SqlBytes
where
    Vec<u8>: Encode<'a, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
    ) -> Result<IsNull, BoxDynError> {
        self.0.to_vec().encode_by_ref(buf)
    }
}

impl<'a, DB: Database> Decode<'a, DB> for SqlBytes
where
    Vec<u8>: Decode<'a, DB>,
{
    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
        let bytes = Vec::<u8>::decode(value)?;
        Ok(SqlBytes(alloy::primitives::Bytes::from(bytes)))
    }
}