ethereum_mysql/
sqlx.rs

1//! This module is only available when the `sqlx` feature is enabled.
2//! Support for the [`sqlx`](https://crates.io/crates/sqlx) crate.
3//!
4//! This implementation encodes and decodes Ethereum types to and from string (hex/decimal) format.
5//!
6//! **Note:** The recommended database column type is `VARCHAR(42)` or `CHAR(42)` (MySQL/SQLite) for addresses,
7//! and `VARCHAR(66)` or `TEXT` for U256 values. This is suitable for cross-language and legacy database integration.
8//!
9//! **U256 string encoding/decoding notes:**
10//! - When writing to the database, U256 is always encoded as a lowercase hex string with `0x` prefix (e.g. `0x1234...`).
11//! - When reading from the database, both `0x`-prefixed hex strings and pure decimal strings are supported.
12//! - For best compatibility and predictable sorting/comparison, always store U256 as hex strings in the database.
13//! - If you store decimal strings, reading is supported, but database-level comparison/sorting may not match Rust-side logic.
14//!
15#![cfg_attr(docsrs, doc(cfg(feature = "sqlx")))]
16
17use std::str::FromStr;
18use thiserror::Error;
19
20use sqlx_core::{
21    database::Database,
22    decode::Decode,
23    encode::{Encode, IsNull},
24    error::BoxDynError,
25    types::Type,
26};
27
28/// Error type for decoding failures when converting database values to Ethereum types.
29///
30/// This is used when a value from the database cannot be represented in the target type,
31/// such as when a byte not a valid Ethereum address or U256 string.
32#[derive(Error, Debug)]
33pub enum DecodeError {
34    /// Returned when the database value is not a valid Ethereum address string.
35    #[error("Address decode error: source {0}")]
36    AddressDecodeError(String),
37
38    /// Returned when the database value is not a valid Uint string.
39    #[error("Uint decode error: source {0}")]
40    UintDecodeError(String),
41
42    /// Returned when the database value is not a valid FixedBytes string.
43    #[error("FixedBytes decode error: source {0}")]
44    FixedBytesDecodeError(String),
45
46    /// Returned when the database value is not a valid Bytes string.
47    #[error("Bytes decode error: source {0}")]
48    BytesDecodeError(String),
49}
50
51use crate::{SqlAddress, SqlBytes, SqlFixedBytes, SqlUint};
52
53// for SqlAddress
54impl<DB: Database> Type<DB> for SqlAddress
55where
56    String: Type<DB>,
57{
58    fn type_info() -> DB::TypeInfo {
59        <String as Type<DB>>::type_info()
60    }
61
62    fn compatible(ty: &DB::TypeInfo) -> bool {
63        <String as Type<DB>>::compatible(ty)
64    }
65}
66
67impl<'a, DB: Database> Encode<'a, DB> for SqlAddress
68where
69    String: Encode<'a, DB>,
70{
71    fn encode_by_ref(
72        &self,
73        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
74    ) -> Result<IsNull, BoxDynError> {
75        self.to_string().to_lowercase().encode_by_ref(buf)
76    }
77}
78
79impl<'a, DB: Database> Decode<'a, DB> for SqlAddress
80where
81    String: Decode<'a, DB>,
82{
83    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
84        let s = String::decode(value)?;
85        SqlAddress::from_str(&s).map_err(|_| DecodeError::AddressDecodeError(s).into())
86    }
87}
88
89// for SqlUint
90impl<const BITS: usize, const LIMBS: usize, DB: Database> Type<DB> for SqlUint<BITS, LIMBS>
91where
92    String: Type<DB>,
93{
94    fn type_info() -> DB::TypeInfo {
95        <String as Type<DB>>::type_info()
96    }
97
98    fn compatible(ty: &DB::TypeInfo) -> bool {
99        <String as Type<DB>>::compatible(ty)
100    }
101}
102
103impl<'a, const BITS: usize, const LIMBS: usize, DB: Database> Encode<'a, DB>
104    for SqlUint<BITS, LIMBS>
105where
106    String: Encode<'a, DB>,
107{
108    fn encode_by_ref(
109        &self,
110        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
111    ) -> Result<IsNull, BoxDynError> {
112        self.to_string().to_lowercase().encode_by_ref(buf)
113    }
114}
115
116impl<'a, const BITS: usize, const LIMBS: usize, DB: Database> Decode<'a, DB>
117    for SqlUint<BITS, LIMBS>
118where
119    String: Decode<'a, DB>,
120{
121    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
122        let s = String::decode(value)?;
123        SqlUint::<BITS, LIMBS>::from_str(&s)
124            .map_err(|_| DecodeError::UintDecodeError(s.to_string()).into())
125    }
126}
127
128/// for SqlFixedBytes<32>
129impl<DB: Database> Type<DB> for SqlFixedBytes<32>
130where
131    String: Type<DB>,
132{
133    fn type_info() -> DB::TypeInfo {
134        <String as Type<DB>>::type_info()
135    }
136
137    fn compatible(ty: &DB::TypeInfo) -> bool {
138        <String as Type<DB>>::compatible(ty)
139    }
140}
141impl<'a, DB: Database> Encode<'a, DB> for SqlFixedBytes<32>
142where
143    String: Encode<'a, DB>,
144{
145    fn encode_by_ref(
146        &self,
147        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
148    ) -> Result<IsNull, BoxDynError> {
149        self.to_string().to_lowercase().encode_by_ref(buf)
150    }
151}
152impl<'a, DB: Database> Decode<'a, DB> for SqlFixedBytes<32>
153where
154    String: Decode<'a, DB>,
155{
156    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
157        let s = String::decode(value)?;
158        SqlFixedBytes::<32>::from_str(&s).map_err(|_| DecodeError::FixedBytesDecodeError(s).into())
159    }
160}
161
162// for SqlBytes
163impl<DB: Database> Type<DB> for SqlBytes
164where
165    String: Type<DB>,
166{
167    fn type_info() -> DB::TypeInfo {
168        <String as Type<DB>>::type_info()
169    }
170
171    fn compatible(ty: &DB::TypeInfo) -> bool {
172        <String as Type<DB>>::compatible(ty)
173    }
174}
175
176impl<'a, DB: Database> Encode<'a, DB> for SqlBytes
177where
178    String: Encode<'a, DB>,
179{
180    fn encode_by_ref(
181        &self,
182        buf: &mut <DB as Database>::ArgumentBuffer<'a>,
183    ) -> Result<IsNull, BoxDynError> {
184        self.to_string().to_lowercase().encode_by_ref(buf)
185    }
186}
187
188impl<'a, DB: Database> Decode<'a, DB> for SqlBytes
189where
190    String: Decode<'a, DB>,
191{
192    fn decode(value: <DB as Database>::ValueRef<'a>) -> Result<Self, BoxDynError> {
193        let s = String::decode(value)?;
194        SqlBytes::from_str(&s).map_err(|e| DecodeError::BytesDecodeError(e.to_string()).into())
195    }
196}