Fix MySQL placeholder issue and add missing Value types

- Remove broken static-validation feature (hardcoded $1 placeholders)
- Add Value::Null variant for Option<T> support
- Add From<Option<T>> impl for all Value types
- Add f32, f64, NaiveTime, serde_json::Value support
- Add optional decimal feature for rust_decimal::Decimal
- All database backends now use runtime placeholder() function

Fixes issues:
- MySQL getting PostgreSQL $1 placeholders
- Missing From<Option<T>> implementations
- Missing base types (Decimal, JsonValue, NaiveTime, floats)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Netshipise 2026-01-28 21:01:31 +02:00
parent ceeecf2e5c
commit 3c0ae1983f
4 changed files with 137 additions and 78 deletions

View File

@ -5,7 +5,7 @@ edition.workspace = true
description = "Entity CRUD and change tracking for SQL databases with SQLx" description = "Entity CRUD and change tracking for SQL databases with SQLx"
[workspace.package] [workspace.package]
version = "0.3.2" version = "0.3.3"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -16,6 +16,7 @@ uuid = { version = "1", features = ["v4"] }
chrono = "0.4" chrono = "0.4"
rand = "0.8" rand = "0.8"
paste = "1.0" paste = "1.0"
rust_decimal = { version = "1", optional = true }
[workspace] [workspace]
members = [ members = [
@ -27,7 +28,7 @@ members = [
[features] [features]
default = [] default = []
derive = ["dep:sqlx-record-derive"] derive = ["dep:sqlx-record-derive"]
static-validation = ["sqlx-record-derive?/static-validation"] decimal = ["dep:rust_decimal", "sqlx/rust_decimal"]
# Database backends - user must enable at least one # Database backends - user must enable at least one
mysql = ["sqlx/mysql", "sqlx-record-derive?/mysql"] mysql = ["sqlx/mysql", "sqlx-record-derive?/mysql"]

View File

@ -13,7 +13,6 @@ futures = "0.3"
[features] [features]
default = [] default = []
static-validation = []
mysql = [] mysql = []
postgres = [] postgres = []
sqlite = [] sqlite = []

View File

@ -563,50 +563,9 @@ fn generate_get_impl(
quote! {} quote! {}
}; };
// Check if static-validation feature is enabled at macro expansion time
let use_static_validation = cfg!(feature = "static-validation");
let get_by_impl = if use_static_validation {
let select_stmt = format!(
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = $1"#,
select_fields.clone().collect::<Vec<_>>().join(", "),
tq, table_name, tq, pk_db_field_name
);
quote! {
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
where
E: sqlx::Executor<'a, Database=#db>,
{
let result = sqlx::query_as!(
Self,
#select_stmt,
#pk_field
)
.fetch_optional(executor)
.await?;
Ok(result)
}
pub async fn get_by_primary_key<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
where
E: sqlx::Executor<'a, Database=#db>,
{
let result = sqlx::query_as!(
Self,
#select_stmt,
#pk_field
)
.fetch_optional(executor)
.await?;
Ok(result)
}
}
} else {
let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>(); let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>();
quote! { let get_by_impl = quote! {
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error> pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database=#db>, E: sqlx::Executor<'a, Database=#db>,
@ -642,7 +601,6 @@ fn generate_get_impl(
Ok(result) Ok(result)
} }
}
}; };
quote! { quote! {

View File

@ -1,5 +1,5 @@
use sqlx::query::{Query, QueryAs, QueryScalar}; use sqlx::query::{Query, QueryAs, QueryScalar};
use sqlx::types::chrono::{NaiveDate, NaiveDateTime}; use sqlx::types::chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use crate::filter::placeholder; use crate::filter::placeholder;
// Database type alias based on enabled feature // Database type alias based on enabled feature
@ -34,6 +34,7 @@ pub type Arguments_<'q> = sqlx::postgres::PgArguments;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Value { pub enum Value {
Null,
Int8(i8), Int8(i8),
Uint8(u8), Uint8(u8),
Int16(i16), Int16(i16),
@ -42,12 +43,18 @@ pub enum Value {
Uint32(u32), Uint32(u32),
Int64(i64), Int64(i64),
Uint64(u64), Uint64(u64),
Float32(f32),
Float64(f64),
VecU8(Vec<u8>), VecU8(Vec<u8>),
String(String), String(String),
Bool(bool), Bool(bool),
Uuid(uuid::Uuid), Uuid(uuid::Uuid),
NaiveDate(NaiveDate), NaiveDate(NaiveDate),
NaiveDateTime(NaiveDateTime), NaiveDateTime(NaiveDateTime),
NaiveTime(NaiveTime),
Json(serde_json::Value),
#[cfg(feature = "decimal")]
Decimal(rust_decimal::Decimal),
} }
/// Expression for column updates beyond simple value assignment. /// Expression for column updates beyond simple value assignment.
@ -253,6 +260,7 @@ pub type SqlValue = Value;
macro_rules! bind_value { macro_rules! bind_value {
($query:expr, $value: expr) => {{ ($query:expr, $value: expr) => {{
let query = match $value { let query = match $value {
Value::Null => $query.bind(None::<String>),
Value::Int8(v) => $query.bind(v), Value::Int8(v) => $query.bind(v),
Value::Uint8(v) => $query.bind(v), Value::Uint8(v) => $query.bind(v),
Value::Int16(v) => $query.bind(v), Value::Int16(v) => $query.bind(v),
@ -261,12 +269,18 @@ macro_rules! bind_value {
Value::Uint32(v) => $query.bind(v), Value::Uint32(v) => $query.bind(v),
Value::Int64(v) => $query.bind(v), Value::Int64(v) => $query.bind(v),
Value::Uint64(v) => $query.bind(v), Value::Uint64(v) => $query.bind(v),
Value::Float32(v) => $query.bind(v),
Value::Float64(v) => $query.bind(v),
Value::VecU8(v) => $query.bind(v), Value::VecU8(v) => $query.bind(v),
Value::String(v) => $query.bind(v), Value::String(v) => $query.bind(v),
Value::Bool(v) => $query.bind(v), Value::Bool(v) => $query.bind(v),
Value::Uuid(v) => $query.bind(v), Value::Uuid(v) => $query.bind(v),
Value::NaiveDate(v) => $query.bind(v), Value::NaiveDate(v) => $query.bind(v),
Value::NaiveDateTime(v) => $query.bind(v), Value::NaiveDateTime(v) => $query.bind(v),
Value::NaiveTime(v) => $query.bind(v),
Value::Json(v) => $query.bind(v),
#[cfg(feature = "decimal")]
Value::Decimal(v) => $query.bind(v),
}; };
query query
}}; }};
@ -277,6 +291,7 @@ macro_rules! bind_value {
macro_rules! bind_value { macro_rules! bind_value {
($query:expr, $value: expr) => {{ ($query:expr, $value: expr) => {{
let query = match $value { let query = match $value {
Value::Null => $query.bind(None::<String>),
Value::Int8(v) => $query.bind(v), Value::Int8(v) => $query.bind(v),
Value::Uint8(v) => $query.bind(*v as i16), Value::Uint8(v) => $query.bind(*v as i16),
Value::Int16(v) => $query.bind(v), Value::Int16(v) => $query.bind(v),
@ -285,12 +300,18 @@ macro_rules! bind_value {
Value::Uint32(v) => $query.bind(*v as i64), Value::Uint32(v) => $query.bind(*v as i64),
Value::Int64(v) => $query.bind(v), Value::Int64(v) => $query.bind(v),
Value::Uint64(v) => $query.bind(*v as i64), Value::Uint64(v) => $query.bind(*v as i64),
Value::Float32(v) => $query.bind(v),
Value::Float64(v) => $query.bind(v),
Value::VecU8(v) => $query.bind(v), Value::VecU8(v) => $query.bind(v),
Value::String(v) => $query.bind(v), Value::String(v) => $query.bind(v),
Value::Bool(v) => $query.bind(v), Value::Bool(v) => $query.bind(v),
Value::Uuid(v) => $query.bind(v), Value::Uuid(v) => $query.bind(v),
Value::NaiveDate(v) => $query.bind(v), Value::NaiveDate(v) => $query.bind(v),
Value::NaiveDateTime(v) => $query.bind(v), Value::NaiveDateTime(v) => $query.bind(v),
Value::NaiveTime(v) => $query.bind(v),
Value::Json(v) => $query.bind(v),
#[cfg(feature = "decimal")]
Value::Decimal(v) => $query.bind(v),
}; };
query query
}}; }};
@ -309,10 +330,13 @@ pub fn bind_values<'q>(query: Query<'q, DB, Arguments_<'q>>, values: &'q [Value]
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))] #[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value) -> Query<'q, DB, Arguments_<'q>> { pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value) -> Query<'q, DB, Arguments_<'q>> {
match value { match value {
Value::Null => query.bind(None::<String>),
Value::Int8(v) => query.bind(v), Value::Int8(v) => query.bind(v),
Value::Int16(v) => query.bind(v), Value::Int16(v) => query.bind(v),
Value::Int32(v) => query.bind(v), Value::Int32(v) => query.bind(v),
Value::Int64(v) => query.bind(v), Value::Int64(v) => query.bind(v),
Value::Float32(v) => query.bind(v),
Value::Float64(v) => query.bind(v),
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
Value::Uint8(v) => query.bind(v), Value::Uint8(v) => query.bind(v),
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
@ -335,6 +359,10 @@ pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value)
Value::Uuid(v) => query.bind(v), Value::Uuid(v) => query.bind(v),
Value::NaiveDate(v) => query.bind(v), Value::NaiveDate(v) => query.bind(v),
Value::NaiveDateTime(v) => query.bind(v), Value::NaiveDateTime(v) => query.bind(v),
Value::NaiveTime(v) => query.bind(v),
Value::Json(v) => query.bind(v),
#[cfg(feature = "decimal")]
Value::Decimal(v) => query.bind(v),
} }
} }
@ -530,6 +558,79 @@ impl From<&NaiveDateTime> for Value {
} }
} }
// New type implementations
impl From<f32> for Value {
fn from(value: f32) -> Self {
Value::Float32(value)
}
}
impl From<&f32> for Value {
fn from(value: &f32) -> Self {
Value::Float32(*value)
}
}
impl From<f64> for Value {
fn from(value: f64) -> Self {
Value::Float64(value)
}
}
impl From<&f64> for Value {
fn from(value: &f64) -> Self {
Value::Float64(*value)
}
}
impl From<NaiveTime> for Value {
fn from(value: NaiveTime) -> Self {
Value::NaiveTime(value)
}
}
impl From<&NaiveTime> for Value {
fn from(value: &NaiveTime) -> Self {
Value::NaiveTime(*value)
}
}
impl From<serde_json::Value> for Value {
fn from(value: serde_json::Value) -> Self {
Value::Json(value)
}
}
impl From<&serde_json::Value> for Value {
fn from(value: &serde_json::Value) -> Self {
Value::Json(value.clone())
}
}
#[cfg(feature = "decimal")]
impl From<rust_decimal::Decimal> for Value {
fn from(value: rust_decimal::Decimal) -> Self {
Value::Decimal(value)
}
}
#[cfg(feature = "decimal")]
impl From<&rust_decimal::Decimal> for Value {
fn from(value: &rust_decimal::Decimal) -> Self {
Value::Decimal(*value)
}
}
// Option<T> implementations - convert None to Value::Null
impl<T: Into<Value>> From<Option<T>> for Value {
fn from(value: Option<T>) -> Self {
match value {
Some(v) => v.into(),
None => Value::Null,
}
}
}
pub trait BindValues<'q> { pub trait BindValues<'q> {
type Output; type Output;