fix: use Value-based binding in UpdateForm for proper Option<T> handling

When UpdateForm wraps fields that are already Option<T>, it creates
nested Options (Option<Option<T>>). The old bind_form_values method
bound these directly as &Option<T>, which caused MySQL "malform packet"
errors for Uuid -> BINARY(16) conversions.

Now both bind_form_values and bind_all_values use update_stmt_with_values()
which properly converts values through the Value enum:
- Some(None) -> Value::Null
- Some(Some(v)) -> Value::T(v)

This preserves the three-state semantics:
- None: don't include field in UPDATE
- Some(None): SET column = NULL
- Some(Some(v)): SET column = value

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Netshipise 2026-01-30 18:13:07 +02:00
parent a1464d3f7c
commit 6ed2401be1
3 changed files with 16 additions and 24 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.6" version = "0.3.7"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -1106,40 +1106,31 @@ fn generate_update_impl(
/// Bind all form values to query in correct order. /// Bind all form values to query in correct order.
/// Handles both simple values and expression values, respecting expression precedence. /// Handles both simple values and expression values, respecting expression precedence.
/// Uses Value enum for proper type handling of Option<T> fields.
pub fn bind_all_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>) pub fn bind_all_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>)
-> sqlx::query::Query<'q, #db, #db_args> -> sqlx::query::Query<'q, #db, #db_args>
{ {
#( // Use update_stmt_with_values to get properly converted values
// Expression takes precedence over simple value // This handles nested Options (Option<Option<T>>) correctly
if let Some(expr) = self._exprs.get(#db_names) { let (_, values) = self.update_stmt_with_values();
let (_, expr_values) = expr.build_sql(#db_names, 1); for value in values {
for value in expr_values {
query = ::sqlx_record::prelude::bind_value_owned(query, value); query = ::sqlx_record::prelude::bind_value_owned(query, value);
} }
} else if let Some(ref value) = self.#field_idents {
query = query.bind(value);
}
)*
query query
} }
/// Legacy binding method - only binds simple Option values (ignores expressions). /// Legacy binding method - binds values through the Value enum for proper type handling.
/// For backward compatibility. New code should use bind_all_values(). /// For backward compatibility. New code should use bind_all_values().
pub fn bind_form_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>) pub fn bind_form_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>)
-> sqlx::query::Query<'q, #db, #db_args> -> sqlx::query::Query<'q, #db, #db_args>
{ {
if self._exprs.is_empty() { // Always use Value-based binding to properly handle Option<T> fields
// No expressions, use simple binding // This ensures nested Options (Option<Option<T>>) are unwrapped correctly
#( let (_, values) = self.update_stmt_with_values();
if let Some(ref value) = self.#field_idents { for value in values {
query = query.bind(value); query = ::sqlx_record::prelude::bind_value_owned(query, value);
} }
)*
query query
} else {
// Has expressions, use full binding
self.bind_all_values(query)
}
} }
/// Check if this form uses any expressions /// Check if this form uses any expressions

View File

@ -256,6 +256,7 @@ impl UpdateExpr {
pub type SqlValue = Value; pub type SqlValue = Value;
// MySQL supports unsigned integers natively // MySQL supports unsigned integers natively
// Note: UUID is bound as bytes for BINARY(16) column compatibility
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
macro_rules! bind_value { macro_rules! bind_value {
($query:expr, $value: expr) => {{ ($query:expr, $value: expr) => {{