From 1349ca8c9eaa7eb72df1a26d61f69f8689a68421 Mon Sep 17 00:00:00 2001 From: Jeremie Zitti Date: Fri, 17 Jul 2026 17:03:56 +0100 Subject: [PATCH] docs(macros): document that bind parameter nullability is not compile-checked Restructures the "Nullability: Bind Parameters" section of the query! macro documentation to state upfront that the nullability of bind parameters is not verified at compile time, why (SQLx does not parse SQL), and give the concrete example from #2642. The previous phrasing understated the limitation and hid it behind guidance about WHERE clauses, which is now demoted to a sub-section. Closes #2642 --- src/macros/mod.rs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 0db6f0c2e7..ef9b6a3a2e 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -130,18 +130,42 @@ /// * MySQL/SQLite: `?` which matches arguments in order that it appears in the query /// /// ## Nullability: Bind Parameters -/// For a given expected type `T`, both `T` and `Option` are allowed (as well as either -/// behind references). `Option::None` will be bound as `NULL`, so if binding a type behind `Option` -/// be sure your query can support it. +/// **The nullability of bind parameters is _not_ verified at compile time.** Unlike output +/// columns (see the [next section](#nullability-output-columns)), the `query!()` family of +/// macros does not check whether an `Option` bound to a parameter is compatible with the +/// nullability of the target column. This is a fundamental limitation, not an oversight: +/// determining which parameter maps to which column would require the macros to parse and +/// analyze the SQL themselves, which SQLx explicitly does not do +/// (see [the FAQ][faq-parse-sql] for the reasoning). +/// +/// For any bind parameter, both `T` and `Option` are accepted (as well as either behind +/// references). `Option::None` is bound as SQL `NULL`. If the target column has a `NOT NULL` +/// constraint, binding `None` will compile successfully but fail **at runtime** with a +/// database error, for example: /// -/// Note, however, if binding in a `where` clause, that equality comparisons with `NULL` may not -/// work as expected; instead you must use `IS NOT NULL` or `IS NULL` to check if a column is not +/// ```rust,ignore +/// // Schema: `CREATE TABLE foo (data TEXT NOT NULL);` +/// // Compiles fine, fails at runtime: +/// sqlx::query!("INSERT INTO foo (data) VALUES ($1)", None::) +/// .execute(&pool) +/// .await?; +/// ``` +/// +/// If you need this kind of safety, encode the constraint in your Rust types (e.g. use `String` +/// instead of `Option` for the field that feeds the bind parameter) or cover the +/// invariant with an integration test against a real database. +/// +/// ### Bind parameters in `WHERE` clauses +/// If binding in a `WHERE` clause, note that equality comparisons with `NULL` may not work +/// as expected; instead you must use `IS NOT NULL` or `IS NULL` to check if a column is not /// null or is null, respectively. /// /// In Postgres and MySQL you may also use `IS [NOT] DISTINCT FROM` to compare with a possibly /// `NULL` value. In MySQL `IS NOT DISTINCT FROM` can be shortened to `<=>`. /// In SQLite you can use `IS` or `IS NOT`. Note that operator precedence may be different. /// +/// [faq-parse-sql]: https://github.com/transact-rs/sqlx/blob/main/FAQ.md#why-cant-sqlx-just-look-at-my-database-schemamigrations-and-parse-the-sql-itself +/// /// ## Nullability: Output Columns /// In most cases, the database engine can tell us whether or not a column may be `NULL`, and /// the `query!()` macro adjusts the field types of the returned struct accordingly.