fix(mysql): infer DATE/DATETIME/TIMESTAMP columns as nullable in query!/query_as! to handle zero dates#4294
Open
Ar-maan05 wants to merge 1 commit into
Open
Conversation
A MySQL zero date (0000-00-00 00:00:00) is decoded as NULL/None, so a NOT NULL DATE/DATETIME/TIMESTAMP column can still yield None at runtime. runtime is_null semantics, fixing UnexpectedNullError at runtime. Fixes transact-rs#4283
Author
|
The one red check: Postgres SSL Auth (17, async-std, native-tls), is an unrelated CI infrastructure flake, not a test failure. The job failed at the setup step:
i.e. the Postgres SSL container failed to start before any test ran. This PR is MySQL-only (the diff touches Happy to push a rebase/empty commit to re-trigger CI if that's easier than a re-run on your end. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Does your PR solve an issue?
Fixes #4283
Is this a breaking change?
Yes: this is a behavior change to compile-time type inference for the
query!family of macros, so it should land in the next major release (0.10.0).query!/query_as!/query_scalar!now infer MySQLDATE,DATETIME, andTIMESTAMPcolumns as nullable (Option<T>) even when the column is declaredNOT NULL. Existing code that binds such a column to a non-Optionfield/struct will need either to useOption<T>or to opt out with the!column override (e.g.SELECT created_at AS "created_at!").TIMEand non-temporal columns are unaffected.Problem
Against a MySQL
NOT NULL TIMESTAMP/DATETIME/DATEcolumn that contains a "zero date" (0000-00-00 00:00:00),query_as!fails at runtime withUnexpectedNullError:Root cause
The runtime is internally consistent and intentionally treats a zero date as
NULL:MySqlValueRef::is_null()(sqlx-mysql/src/value.rs) maps zeroDATE/DATETIME/TIMESTAMPvalues toNULL, soOption<NaiveDateTime>already decodes them toNone(and a non-Optiondecode yieldsUnexpectedNullError, asserted bytest_type_chrono_zero_date). This was deliberate (08247237 "handle zero dates in MySQL, emit as Option::None (treat as NULL)").The gap is in macro nullability inference:
describe()insqlx-mysql/src/connection/executor.rsderived nullability purely from theNOT_NULLcolumn flag. So aNOT NULLtemporal column was inferred non-nullable, the macro generated a bareNaiveDateTimefield, and a zero date then decoded as a "null" into a non-Optionfield ->UnexpectedNullError. MySQL has no other nullability path (unlike Postgres'EXPLAIN-based refinement), so this one site is authoritative.Fix
Treat
DATE/DATETIME/TIMESTAMPcolumns as nullable in inference regardless ofNOT_NULL, matching the exact type set thatis_null()already maps toNULL. The macros now emitOption<T>for these columns, which decodes both real values (Some) and zero dates (None) correctly.TIMEis excluded because it has no zero value that maps toNULL.Tests
tests/mysql/describe.rs: newit_describes_temporal_not_null_as_nullableassertingDATE/DATETIME/TIMESTAMP NOT NULLinfer as nullable whileTIME NOT NULLandINTEGER NOT NULLstay non-nullable; updatedit_describes_simplefor thecreated_at TIMESTAMPcolumn.tests/mysql/macros.rs: updatedtest_uuid_is_compatible_mariadb(created_at→Option<OffsetDateTime>).Alternative considered
A
sql_mode-aware variant, only inferring nullable when the session actually permits zero dates (NO_ZERO_DATE/NO_ZERO_IN_DATEoff), would avoid the ergonomic cost for strict-mode users. I went with the unconditional rule because it's always safe, whereas thesql_modeapproach can still produce a runtimeUnexpectedNullErrorwhen the compile-time DB is strict but production is permissive. Happy to switch if you'd prefer the narrower behavior.