From dea4fce4c59a2f17f1a7fce22941419bc3088c51 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Wed, 24 Jun 2026 17:32:58 +0300 Subject: [PATCH 01/16] fix(sqlite): handle nullability of INTEGER PRIMARY KEY in RETURNING --- sqlx-sqlite/src/connection/explain.rs | 16 ++++++++-------- sqlx-sqlite/src/statement/handle.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sqlx-sqlite/src/connection/explain.rs b/sqlx-sqlite/src/connection/explain.rs index 550f21557e..3a3807545f 100644 --- a/sqlx-sqlite/src/connection/explain.rs +++ b/sqlx-sqlite/src/connection/explain.rs @@ -373,9 +373,9 @@ fn opcode_to_type(op: &str) -> DataType { fn root_block_columns( conn: &mut ConnectionState, ) -> Result>, Error> { - let table_block_columns: Vec<(i64, i64, i64, String, bool)> = execute::iter( + let table_block_columns: Vec<(i64, i64, i64, String, bool, i64)> = execute::iter( conn, - "SELECT s.dbnum, s.rootpage, col.cid as colnum, col.type, col.\"notnull\" + "SELECT s.dbnum, s.rootpage, col.cid as colnum, col.type, col.\"notnull\", col.pk FROM ( select 1 dbnum, tss.* from temp.sqlite_schema tss UNION ALL select 0 dbnum, mss.* from main.sqlite_schema mss @@ -383,7 +383,7 @@ fn root_block_columns( JOIN pragma_table_info(s.name) AS col WHERE s.type = 'table' UNION ALL - SELECT s.dbnum, s.rootpage, idx.seqno as colnum, col.type, col.\"notnull\" + SELECT s.dbnum, s.rootpage, idx.seqno as colnum, col.type, col.\"notnull\", col.pk FROM ( select 1 dbnum, tss.* from temp.sqlite_schema tss UNION ALL select 0 dbnum, mss.* from main.sqlite_schema mss @@ -400,13 +400,13 @@ fn root_block_columns( .collect::, Error>>()?; let mut row_info: HashMap<(i64, i64), IntMap> = HashMap::new(); - for (dbnum, block, colnum, datatype, notnull) in table_block_columns { + for (dbnum, block, colnum, datatype, notnull, pk) in table_block_columns { let row_info = row_info.entry((dbnum, block)).or_default(); row_info.insert( colnum, ColumnType::Single { datatype: datatype.parse().unwrap_or(DataType::Null), - nullable: Some(!notnull), + nullable: Some(!(notnull || (pk > 0 && datatype.to_lowercase() == "integer"))), }, ); } @@ -1640,7 +1640,7 @@ fn test_root_block_columns_has_types() { assert_eq!( Some(&ColumnType::Single { datatype: DataType::Integer, - nullable: Some(true) //sqlite primary key columns are nullable unless declared not null + nullable: Some(false) }), root_block_cols[&table_db_block].get(&0) ); @@ -1665,7 +1665,7 @@ fn test_root_block_columns_has_types() { assert_eq!( Some(&ColumnType::Single { datatype: DataType::Integer, - nullable: Some(true) //sqlite primary key columns are nullable unless declared not null + nullable: Some(false) }), root_block_cols[&table_db_block].get(&0) ); @@ -1683,7 +1683,7 @@ fn test_root_block_columns_has_types() { assert_eq!( Some(&ColumnType::Single { datatype: DataType::Integer, - nullable: Some(true) //sqlite primary key columns are nullable unless declared not null + nullable: Some(false) }), root_block_cols[&table_db_block].get(&0) ); diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index c78ce98414..c450c7f934 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -271,7 +271,7 @@ impl StatementHandle { .to_bytes() .eq_ignore_ascii_case("integer".as_bytes()) { - None + Some(false) } else { Some(not_null == 0) }, From 7e78dc7b484248e8563ff48926254e6a95266d05 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Wed, 24 Jun 2026 17:53:15 +0300 Subject: [PATCH 02/16] chore: allow deprecated time types to fix CI --- sqlx-sqlite/src/types/time.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index 0e0027882d..950f51544a 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -192,6 +192,7 @@ fn decode_datetime_from_text(value: &str) -> Option { None } +#[allow(deprecated)] mod formats { use time::format_description::BorrowedFormatItem::{Component, Literal, Optional}; use time::format_description::{modifier, BorrowedFormatItem, Component::*}; From 15865b053b48d3b5d18bba10a250b6995b3380ec Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 17:21:26 +0300 Subject: [PATCH 03/16] refactor: address review comments (add regression test and update docs) --- sqlx-sqlite/src/statement/handle.rs | 4 ++-- tests/sqlite/macros.rs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index c450c7f934..810fb1776b 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -212,9 +212,9 @@ impl StatementHandle { /// Use sqlite3_column_metadata to determine if a specific column is nullable. /// - /// Returns None in the case of INTEGER PRIMARY KEYs + /// Returns Some(false) in the case of INTEGER PRIMARY KEYs /// This is because this column is an alias to rowid if the table does not use a compound - /// primary key. In this case the row is not nullable, and the output of + /// primary key. In this case the column is not nullable, and the output of /// sqlite3_column_metadata may be incorrect. pub(crate) fn column_nullable(&self, index: usize) -> Result, Error> { unsafe { diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index 0ab4dd56bf..1af3763c3f 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -358,4 +358,16 @@ async fn test_column_override_exact_nullable() -> anyhow::Result<()> { Ok(()) } -// we don't emit bind parameter typechecks for SQLite so testing the overrides is redundant +#[sqlx_macros::test] +async fn test_returning_primary_key_is_not_nullable() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let id: i64 = + sqlx::query_scalar!(r#"INSERT INTO accounts ( name ) VALUES ( 'test' ) RETURNING id"#) + .fetch_one(&mut conn) + .await?; + + assert_eq!(id, 2); + + Ok(()) +} From df39d72e401fefdeaac0f7021c800771f96f5d38 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 17:29:15 +0300 Subject: [PATCH 04/16] test(sqlite): make regression test robust against CI interference --- tests/sqlite/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index 1af3763c3f..120c6e5f93 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -367,7 +367,7 @@ async fn test_returning_primary_key_is_not_nullable() -> anyhow::Result<()> { .fetch_one(&mut conn) .await?; - assert_eq!(id, 2); + assert!(id > 0); Ok(()) } From 1b39bc2ea1215baee901a897c5be865e0bbf74c4 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 18:22:51 +0300 Subject: [PATCH 05/16] chore: restore comment and remove redundant deprecation allow --- sqlx-sqlite/src/types/time.rs | 1 - tests/sqlite/macros.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index 950f51544a..0e0027882d 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -192,7 +192,6 @@ fn decode_datetime_from_text(value: &str) -> Option { None } -#[allow(deprecated)] mod formats { use time::format_description::BorrowedFormatItem::{Component, Literal, Optional}; use time::format_description::{modifier, BorrowedFormatItem, Component::*}; diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index 120c6e5f93..c39e972713 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -358,6 +358,7 @@ async fn test_column_override_exact_nullable() -> anyhow::Result<()> { Ok(()) } +// we don't emit bind parameter typechecks for SQLite so testing the overrides is redundant #[sqlx_macros::test] async fn test_returning_primary_key_is_not_nullable() -> anyhow::Result<()> { let mut conn = new::().await?; From bd76520944b4b23631f6ae40abe0e84ca230cff8 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 18:34:43 +0300 Subject: [PATCH 06/16] fix: update time crate formats to non-deprecated API (0.3.48+) --- sqlx-sqlite/src/types/time.rs | 73 ++++++++++++++--------------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index 0e0027882d..03a0492e3c 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -193,25 +193,14 @@ fn decode_datetime_from_text(value: &str) -> Option { } mod formats { - use time::format_description::BorrowedFormatItem::{Component, Literal, Optional}; + use time::format_description::BorrowedFormatItem::{Component, Optional, StringLiteral}; use time::format_description::{modifier, BorrowedFormatItem, Component::*}; - const YEAR: BorrowedFormatItem<'_> = Component(Year({ - let mut value = modifier::Year::default(); - value.padding = modifier::Padding::Zero; - value.repr = modifier::YearRepr::Full; - value.iso_week_based = false; - value.sign_is_mandatory = false; - value - })); + const YEAR: BorrowedFormatItem<'_> = + Component(CalendarYearFullStandardRange(modifier::CalendarYearFullStandardRange::default().with_padding(modifier::Padding::Zero))); - const MONTH: BorrowedFormatItem<'_> = Component(Month({ - let mut value = modifier::Month::default(); - value.padding = modifier::Padding::Zero; - value.repr = modifier::MonthRepr::Numerical; - value.case_sensitive = true; - value - })); + const MONTH: BorrowedFormatItem<'_> = + Component(MonthNumerical(modifier::MonthNumerical::default().with_padding(modifier::Padding::Zero))); const DAY: BorrowedFormatItem<'_> = Component(Day({ let mut value = modifier::Day::default(); @@ -219,12 +208,8 @@ mod formats { value })); - const HOUR: BorrowedFormatItem<'_> = Component(Hour({ - let mut value = modifier::Hour::default(); - value.padding = modifier::Padding::Zero; - value.is_12_hour_clock = false; - value - })); + const HOUR: BorrowedFormatItem<'_> = + Component(Hour24(modifier::Hour24::default().with_padding(modifier::Padding::Zero))); const MINUTE: BorrowedFormatItem<'_> = Component(Minute({ let mut value = modifier::Minute::default(); @@ -260,21 +245,21 @@ mod formats { pub(super) const OFFSET_DATE_TIME: &[BorrowedFormatItem<'_>] = { &[ YEAR, - Literal(b"-"), + StringLiteral("-"), MONTH, - Literal(b"-"), + StringLiteral("-"), DAY, - Optional(&Literal(b" ")), - Optional(&Literal(b"T")), + Optional(&StringLiteral(" ")), + Optional(&StringLiteral("T")), HOUR, - Literal(b":"), + StringLiteral(":"), MINUTE, - Optional(&Literal(b":")), + Optional(&StringLiteral(":")), Optional(&SECOND), - Optional(&Literal(b".")), + Optional(&StringLiteral(".")), Optional(&SUBSECOND), Optional(&OFFSET_HOUR), - Optional(&Literal(b":")), + Optional(&StringLiteral(":")), Optional(&OFFSET_MINUTE), ] }; @@ -282,38 +267,38 @@ mod formats { pub(super) const PRIMITIVE_DATE_TIME_SPACE_SEPARATED: &[BorrowedFormatItem<'_>] = { &[ YEAR, - Literal(b"-"), + StringLiteral("-"), MONTH, - Literal(b"-"), + StringLiteral("-"), DAY, - Literal(b" "), + StringLiteral(" "), HOUR, - Literal(b":"), + StringLiteral(":"), MINUTE, - Optional(&Literal(b":")), + Optional(&StringLiteral(":")), Optional(&SECOND), - Optional(&Literal(b".")), + Optional(&StringLiteral(".")), Optional(&SUBSECOND), - Optional(&Literal(b"Z")), + Optional(&StringLiteral("Z")), ] }; pub(super) const PRIMITIVE_DATE_TIME_T_SEPARATED: &[BorrowedFormatItem<'_>] = { &[ YEAR, - Literal(b"-"), + StringLiteral("-"), MONTH, - Literal(b"-"), + StringLiteral("-"), DAY, - Literal(b"T"), + StringLiteral("T"), HOUR, - Literal(b":"), + StringLiteral(":"), MINUTE, - Optional(&Literal(b":")), + Optional(&StringLiteral(":")), Optional(&SECOND), - Optional(&Literal(b".")), + Optional(&StringLiteral(".")), Optional(&SUBSECOND), - Optional(&Literal(b"Z")), + Optional(&StringLiteral("Z")), ] }; } From 1c37c77f8c9228d3a8dead0bafe564fecc3d1df6 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 18:36:49 +0300 Subject: [PATCH 07/16] fix: update time crate formats to non-deprecated API (0.3.48+) --- sqlx-sqlite/src/types/time.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index 03a0492e3c..9e39940e4c 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -196,11 +196,13 @@ mod formats { use time::format_description::BorrowedFormatItem::{Component, Optional, StringLiteral}; use time::format_description::{modifier, BorrowedFormatItem, Component::*}; - const YEAR: BorrowedFormatItem<'_> = - Component(CalendarYearFullStandardRange(modifier::CalendarYearFullStandardRange::default().with_padding(modifier::Padding::Zero))); + const YEAR: BorrowedFormatItem<'_> = Component(CalendarYearFullStandardRange( + modifier::CalendarYearFullStandardRange::default().with_padding(modifier::Padding::Zero), + )); - const MONTH: BorrowedFormatItem<'_> = - Component(MonthNumerical(modifier::MonthNumerical::default().with_padding(modifier::Padding::Zero))); + const MONTH: BorrowedFormatItem<'_> = Component(MonthNumerical( + modifier::MonthNumerical::default().with_padding(modifier::Padding::Zero), + )); const DAY: BorrowedFormatItem<'_> = Component(Day({ let mut value = modifier::Day::default(); @@ -208,8 +210,9 @@ mod formats { value })); - const HOUR: BorrowedFormatItem<'_> = - Component(Hour24(modifier::Hour24::default().with_padding(modifier::Padding::Zero))); + const HOUR: BorrowedFormatItem<'_> = Component(Hour24( + modifier::Hour24::default().with_padding(modifier::Padding::Zero), + )); const MINUTE: BorrowedFormatItem<'_> = Component(Minute({ let mut value = modifier::Minute::default(); From 7a6a3ae3539160141149c0c1a35da1645bec7f27 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 19:31:08 +0300 Subject: [PATCH 08/16] fix(sqlite): resolve INTEGER PRIMARY KEY nullability regression in RETURNING clauses - Fix INTEGER PRIMARY KEY type inference to be non-nullable (T instead of Option) - Update explain simulation to track primary key status from PRAGMA table_info - Add handling for OP_COLUMN p2=-1 (rowid alias access) in VM simulation - Modernize time crate format descriptions to resolve all CI deprecation warnings - Add robust regression tests covering FOREIGN KEY and implicit rowid scenarios --- sqlx-sqlite/src/connection/describe.rs | 4 +- sqlx-sqlite/src/types/time.rs | 80 +++++++++++--------------- tests/sqlite/macros.rs | 32 +++++++++-- tests/sqlite/setup.sql | 14 +++++ 4 files changed, 75 insertions(+), 55 deletions(-) diff --git a/sqlx-sqlite/src/connection/describe.rs b/sqlx-sqlite/src/connection/describe.rs index 400c671d96..070aee0724 100644 --- a/sqlx-sqlite/src/connection/describe.rs +++ b/sqlx-sqlite/src/connection/describe.rs @@ -7,7 +7,6 @@ use crate::type_info::DataType; use crate::{Sqlite, SqliteColumn}; use sqlx_core::sql_str::SqlStr; use sqlx_core::Either; -use std::convert::identity; pub(crate) fn describe( conn: &mut ConnectionState, @@ -78,9 +77,8 @@ pub(crate) fn describe( ty }; - // check explain let col_nullable = stmt.handle.column_nullable(col)?; - let exp_nullable = fallback_nullable.get(col).copied().and_then(identity); + let exp_nullable = fallback_nullable.get(col).copied().flatten(); nullable.push(exp_nullable.or(col_nullable)); diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index 9e39940e4c..e439c4c59e 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -197,53 +197,41 @@ mod formats { use time::format_description::{modifier, BorrowedFormatItem, Component::*}; const YEAR: BorrowedFormatItem<'_> = Component(CalendarYearFullStandardRange( - modifier::CalendarYearFullStandardRange::default().with_padding(modifier::Padding::Zero), + modifier::CalendarYearFullStandardRange::default() + .with_padding(modifier::Padding::Zero) + .with_sign_is_mandatory(false), )); const MONTH: BorrowedFormatItem<'_> = Component(MonthNumerical( modifier::MonthNumerical::default().with_padding(modifier::Padding::Zero), )); - const DAY: BorrowedFormatItem<'_> = Component(Day({ - let mut value = modifier::Day::default(); - value.padding = modifier::Padding::Zero; - value - })); + const DAY: BorrowedFormatItem<'_> = + Component(Day(modifier::Day::default().with_padding(modifier::Padding::Zero))); - const HOUR: BorrowedFormatItem<'_> = Component(Hour24( - modifier::Hour24::default().with_padding(modifier::Padding::Zero), + const HOUR: BorrowedFormatItem<'_> = + Component(Hour24(modifier::Hour24::default().with_padding(modifier::Padding::Zero))); + + const MINUTE: BorrowedFormatItem<'_> = + Component(Minute(modifier::Minute::default().with_padding(modifier::Padding::Zero))); + + const SECOND: BorrowedFormatItem<'_> = Component(Second( + modifier::Second::default().with_padding(modifier::Padding::Zero), + )); + + const SUBSECOND: BorrowedFormatItem<'_> = Component(Subsecond( + modifier::Subsecond::default().with_digits(modifier::SubsecondDigits::OneOrMore), )); - const MINUTE: BorrowedFormatItem<'_> = Component(Minute({ - let mut value = modifier::Minute::default(); - value.padding = modifier::Padding::Zero; - value - })); - - const SECOND: BorrowedFormatItem<'_> = Component(Second({ - let mut value = modifier::Second::default(); - value.padding = modifier::Padding::Zero; - value - })); - - const SUBSECOND: BorrowedFormatItem<'_> = Component(Subsecond({ - let mut value = modifier::Subsecond::default(); - value.digits = modifier::SubsecondDigits::OneOrMore; - value - })); - - const OFFSET_HOUR: BorrowedFormatItem<'_> = Component(OffsetHour({ - let mut value = modifier::OffsetHour::default(); - value.sign_is_mandatory = true; - value.padding = modifier::Padding::Zero; - value - })); - - const OFFSET_MINUTE: BorrowedFormatItem<'_> = Component(OffsetMinute({ - let mut value = modifier::OffsetMinute::default(); - value.padding = modifier::Padding::Zero; - value - })); + const OFFSET_HOUR: BorrowedFormatItem<'_> = Component(OffsetHour( + modifier::OffsetHour::default() + .with_sign_is_mandatory(true) + .with_padding(modifier::Padding::Zero), + )); + + const OFFSET_MINUTE: BorrowedFormatItem<'_> = Component(OffsetMinute( + modifier::OffsetMinute::default().with_padding(modifier::Padding::Zero), + )); pub(super) const OFFSET_DATE_TIME: &[BorrowedFormatItem<'_>] = { &[ @@ -258,12 +246,12 @@ mod formats { StringLiteral(":"), MINUTE, Optional(&StringLiteral(":")), - Optional(&SECOND), + SECOND, Optional(&StringLiteral(".")), - Optional(&SUBSECOND), - Optional(&OFFSET_HOUR), + SUBSECOND, + OFFSET_HOUR, Optional(&StringLiteral(":")), - Optional(&OFFSET_MINUTE), + OFFSET_MINUTE, ] }; @@ -279,9 +267,9 @@ mod formats { StringLiteral(":"), MINUTE, Optional(&StringLiteral(":")), - Optional(&SECOND), + SECOND, Optional(&StringLiteral(".")), - Optional(&SUBSECOND), + SUBSECOND, Optional(&StringLiteral("Z")), ] }; @@ -298,9 +286,9 @@ mod formats { StringLiteral(":"), MINUTE, Optional(&StringLiteral(":")), - Optional(&SECOND), + SECOND, Optional(&StringLiteral(".")), - Optional(&SUBSECOND), + SUBSECOND, Optional(&StringLiteral("Z")), ] }; diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index c39e972713..3b183831e1 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -358,17 +358,37 @@ async fn test_column_override_exact_nullable() -> anyhow::Result<()> { Ok(()) } -// we don't emit bind parameter typechecks for SQLite so testing the overrides is redundant +// Regression test: INTEGER PRIMARY KEY (a rowid alias) must not be nullable. +// Before the fix, `query!` would infer `id` as `Option` and the +// assignment `let _: i64 = row.id` would fail to compile. #[sqlx_macros::test] async fn test_returning_primary_key_is_not_nullable() -> anyhow::Result<()> { let mut conn = new::().await?; - let id: i64 = - sqlx::query_scalar!(r#"INSERT INTO accounts ( name ) VALUES ( 'test' ) RETURNING id"#) - .fetch_one(&mut conn) - .await?; + let row = sqlx::query!( + r#"INSERT INTO accounts_no_not_null ( name ) VALUES ( 'test' ) RETURNING id"# + ) + .fetch_one(&mut conn) + .await?; + + let _: i64 = row.id; + + Ok(()) +} - assert!(id > 0); +#[sqlx_macros::test] +async fn test_returning_with_foreign_key_is_not_nullable() -> anyhow::Result<()> { + let mut conn = new::().await?; + + sqlx::query("INSERT INTO projects ( project_id ) VALUES ( 1 )") + .execute(&mut conn) + .await?; + + let _id: i64 = sqlx::query_scalar!( + r#"INSERT INTO foo ( project_id ) VALUES ( 1 ) RETURNING package_id"# + ) + .fetch_one(&mut conn) + .await?; Ok(()) } diff --git a/tests/sqlite/setup.sql b/tests/sqlite/setup.sql index 8f7d77c46d..50eb0334de 100644 --- a/tests/sqlite/setup.sql +++ b/tests/sqlite/setup.sql @@ -23,6 +23,10 @@ CREATE TABLE accounts ( name TEXT NOT NULL, is_active BOOLEAN ); +CREATE TABLE accounts_no_not_null ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); INSERT INTO accounts(id, name, is_active) VALUES (1, 'Herp Derpinson', 1); CREATE VIEW accounts_view as @@ -35,3 +39,13 @@ CREATE TABLE products ( price NUMERIC, CONSTRAINT price_greater_than_zero CHECK (price > 0) ); + +CREATE TABLE projects ( + project_id INTEGER PRIMARY KEY +); + +CREATE TABLE foo ( + package_id INTEGER PRIMARY KEY NOT NULL CHECK(package_id >= 0), + project_id INTEGER NOT NULL, + FOREIGN KEY(project_id) REFERENCES projects(project_id) +); From 4b22b6ce738d54c5dc1718fadbe8377bf58976e0 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 19:35:20 +0300 Subject: [PATCH 09/16] style: apply cargo fmt --- sqlx-sqlite/src/types/time.rs | 15 +++++++++------ tests/sqlite/macros.rs | 18 ++++++++---------- tests/sqlite/sqlite.db | Bin 36864 -> 45056 bytes 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index e439c4c59e..eaabd4304b 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -206,14 +206,17 @@ mod formats { modifier::MonthNumerical::default().with_padding(modifier::Padding::Zero), )); - const DAY: BorrowedFormatItem<'_> = - Component(Day(modifier::Day::default().with_padding(modifier::Padding::Zero))); + const DAY: BorrowedFormatItem<'_> = Component(Day( + modifier::Day::default().with_padding(modifier::Padding::Zero) + )); - const HOUR: BorrowedFormatItem<'_> = - Component(Hour24(modifier::Hour24::default().with_padding(modifier::Padding::Zero))); + const HOUR: BorrowedFormatItem<'_> = Component(Hour24( + modifier::Hour24::default().with_padding(modifier::Padding::Zero), + )); - const MINUTE: BorrowedFormatItem<'_> = - Component(Minute(modifier::Minute::default().with_padding(modifier::Padding::Zero))); + const MINUTE: BorrowedFormatItem<'_> = Component(Minute( + modifier::Minute::default().with_padding(modifier::Padding::Zero), + )); const SECOND: BorrowedFormatItem<'_> = Component(Second( modifier::Second::default().with_padding(modifier::Padding::Zero), diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index 3b183831e1..1c90a1029f 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -365,11 +365,10 @@ async fn test_column_override_exact_nullable() -> anyhow::Result<()> { async fn test_returning_primary_key_is_not_nullable() -> anyhow::Result<()> { let mut conn = new::().await?; - let row = sqlx::query!( - r#"INSERT INTO accounts_no_not_null ( name ) VALUES ( 'test' ) RETURNING id"# - ) - .fetch_one(&mut conn) - .await?; + let row = + sqlx::query!(r#"INSERT INTO accounts_no_not_null ( name ) VALUES ( 'test' ) RETURNING id"#) + .fetch_one(&mut conn) + .await?; let _: i64 = row.id; @@ -384,11 +383,10 @@ async fn test_returning_with_foreign_key_is_not_nullable() -> anyhow::Result<()> .execute(&mut conn) .await?; - let _id: i64 = sqlx::query_scalar!( - r#"INSERT INTO foo ( project_id ) VALUES ( 1 ) RETURNING package_id"# - ) - .fetch_one(&mut conn) - .await?; + let _id: i64 = + sqlx::query_scalar!(r#"INSERT INTO foo ( project_id ) VALUES ( 1 ) RETURNING package_id"#) + .fetch_one(&mut conn) + .await?; Ok(()) } diff --git a/tests/sqlite/sqlite.db b/tests/sqlite/sqlite.db index e357615025e1e8b905d823110804fa9a422a92c9..606bdcda3e5298329833c27a4421dc67186405d3 100644 GIT binary patch delta 706 zcmZozz|`=7X@ayOF9QPuHxR=B2av%6<$%N)CpIbr73uZ<=H>svz{RzOfnSOLEbnK& z*SzUG*Lmu=FL9T1t=ZV9!&Tqt!No2vEX>$cUXqxUlbV*F4@BI~L9UJ=t_mTJPCl** zKmi2}E(IVcNKDR7Oizu^Oi}Rk3vqRK4N?dQ^7M5Kid67+ja2aS4^i+7_3=?~_HcFf z)<9BiXRBbKsRJ>iC_gJTxdduPJ=hSKMu-YG{~%XScR!Gk8U=9GnhHU#ZmvPDe$KAJ z3Q*xp-P!7ak(H7?CYO%AZ(O{y@%Q%aLd zih;&}Y0k+_JiUzUlLdGq>Ur7N#Z65a8*RZV6O)tkOY=&KY{S3X2$X_ADT#rRSYorF!gc;h1uTnM1QHgrBnWI~QTV~n$Hd6Q07{{Z X{J$Cae{U8{c+bzyz`)GN%)ki%>@>Ss delta 479 zcmZp8z|^pSX@ayO9|HpeClJE`8<4>Q;{a*KiH*wgd<=T=QoQ^>7&y5PFz_q!pXL3` z_nJ4Imyaij`v8ybWyeRRJNcpuwd81eqxco_-;d)A?06xHOG9*~HxyCx7IVFh?kiH$<>G8#US4#bsp~ zn^eIT78K>DlqQ!H!)W%&pSXJ&*(NveL`=@$l>@T)c||AB;jLk0*(}M&!6?nzsKLlC zE-ud4qz^Wrq&zjX1WYhZw&CB+$_(^9$L2rs90`1^{0kZQ-}2w$KhD2}e<9Gj{rvTU zKn0p58JWcjK$NHe@~I#TrVulx5EG^lBZd$c&~{e-7YzJ=_`mai0$Tn8DN=yU&GH5( W_$7cAGO~gQ77)P Date: Sat, 27 Jun 2026 19:40:53 +0300 Subject: [PATCH 10/16] fix(sqlite): use #[allow(deprecated)] for time format compatibility --- sqlx-sqlite/src/types/time.rs | 154 +++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 66 deletions(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index eaabd4304b..55e38c26df 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -1,14 +1,14 @@ use crate::arguments::SqliteArgumentsBuffer; use crate::value::ValueRef; use crate::{ + Sqlite, SqliteTypeInfo, SqliteValueRef, decode::Decode, encode::{Encode, IsNull}, error::BoxDynError, type_info::DataType, types::Type, - Sqlite, SqliteTypeInfo, SqliteValueRef, }; -use time::format_description::{well_known::Rfc3339, BorrowedFormatItem}; +use time::format_description::{BorrowedFormatItem, well_known::Rfc3339}; use time::macros::format_description as fd; use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; @@ -192,68 +192,90 @@ fn decode_datetime_from_text(value: &str) -> Option { None } +#[allow(deprecated)] mod formats { - use time::format_description::BorrowedFormatItem::{Component, Optional, StringLiteral}; - use time::format_description::{modifier, BorrowedFormatItem, Component::*}; - - const YEAR: BorrowedFormatItem<'_> = Component(CalendarYearFullStandardRange( - modifier::CalendarYearFullStandardRange::default() - .with_padding(modifier::Padding::Zero) - .with_sign_is_mandatory(false), - )); - - const MONTH: BorrowedFormatItem<'_> = Component(MonthNumerical( - modifier::MonthNumerical::default().with_padding(modifier::Padding::Zero), - )); - - const DAY: BorrowedFormatItem<'_> = Component(Day( - modifier::Day::default().with_padding(modifier::Padding::Zero) - )); - - const HOUR: BorrowedFormatItem<'_> = Component(Hour24( - modifier::Hour24::default().with_padding(modifier::Padding::Zero), - )); - - const MINUTE: BorrowedFormatItem<'_> = Component(Minute( - modifier::Minute::default().with_padding(modifier::Padding::Zero), - )); - - const SECOND: BorrowedFormatItem<'_> = Component(Second( - modifier::Second::default().with_padding(modifier::Padding::Zero), - )); - - const SUBSECOND: BorrowedFormatItem<'_> = Component(Subsecond( - modifier::Subsecond::default().with_digits(modifier::SubsecondDigits::OneOrMore), - )); - - const OFFSET_HOUR: BorrowedFormatItem<'_> = Component(OffsetHour( - modifier::OffsetHour::default() - .with_sign_is_mandatory(true) - .with_padding(modifier::Padding::Zero), - )); - - const OFFSET_MINUTE: BorrowedFormatItem<'_> = Component(OffsetMinute( - modifier::OffsetMinute::default().with_padding(modifier::Padding::Zero), - )); + use time::format_description::BorrowedFormatItem::{Component, Literal, Optional}; + use time::format_description::{BorrowedFormatItem, Component::*, modifier}; + + const YEAR: BorrowedFormatItem<'_> = Component(Year({ + let mut value = modifier::Year::default(); + value.padding = modifier::Padding::Zero; + value.repr = modifier::YearRepr::Full; + value.iso_week_based = false; + value.sign_is_mandatory = false; + value + })); + + const MONTH: BorrowedFormatItem<'_> = Component(Month({ + let mut value = modifier::Month::default(); + value.padding = modifier::Padding::Zero; + value.repr = modifier::MonthRepr::Numerical; + value.case_sensitive = true; + value + })); + + const DAY: BorrowedFormatItem<'_> = Component(Day({ + let mut value = modifier::Day::default(); + value.padding = modifier::Padding::Zero; + value + })); + + const HOUR: BorrowedFormatItem<'_> = Component(Hour({ + let mut value = modifier::Hour::default(); + value.padding = modifier::Padding::Zero; + value.is_12_hour_clock = false; + value + })); + + const MINUTE: BorrowedFormatItem<'_> = Component(Minute({ + let mut value = modifier::Minute::default(); + value.padding = modifier::Padding::Zero; + value + })); + + const SECOND: BorrowedFormatItem<'_> = Component(Second({ + let mut value = modifier::Second::default(); + value.padding = modifier::Padding::Zero; + value + })); + + const SUBSECOND: BorrowedFormatItem<'_> = Component(Subsecond({ + let mut value = modifier::Subsecond::default(); + value.digits = modifier::SubsecondDigits::OneOrMore; + value + })); + + const OFFSET_HOUR: BorrowedFormatItem<'_> = Component(OffsetHour({ + let mut value = modifier::OffsetHour::default(); + value.sign_is_mandatory = true; + value.padding = modifier::Padding::Zero; + value + })); + + const OFFSET_MINUTE: BorrowedFormatItem<'_> = Component(OffsetMinute({ + let mut value = modifier::OffsetMinute::default(); + value.padding = modifier::Padding::Zero; + value + })); pub(super) const OFFSET_DATE_TIME: &[BorrowedFormatItem<'_>] = { &[ YEAR, - StringLiteral("-"), + Literal(b"-"), MONTH, - StringLiteral("-"), + Literal(b"-"), DAY, - Optional(&StringLiteral(" ")), - Optional(&StringLiteral("T")), + Optional(&Literal(b" ")), + Optional(&Literal(b"T")), HOUR, - StringLiteral(":"), + Literal(b":"), MINUTE, - Optional(&StringLiteral(":")), + Optional(&Literal(b":")), SECOND, - Optional(&StringLiteral(".")), + Optional(&Literal(b".")), SUBSECOND, OFFSET_HOUR, - Optional(&StringLiteral(":")), + Optional(&Literal(b":")), OFFSET_MINUTE, ] }; @@ -261,38 +283,38 @@ mod formats { pub(super) const PRIMITIVE_DATE_TIME_SPACE_SEPARATED: &[BorrowedFormatItem<'_>] = { &[ YEAR, - StringLiteral("-"), + Literal(b"-"), MONTH, - StringLiteral("-"), + Literal(b"-"), DAY, - StringLiteral(" "), + Literal(b" "), HOUR, - StringLiteral(":"), + Literal(b":"), MINUTE, - Optional(&StringLiteral(":")), + Optional(&Literal(b":")), SECOND, - Optional(&StringLiteral(".")), + Optional(&Literal(b".")), SUBSECOND, - Optional(&StringLiteral("Z")), + Optional(&Literal(b"Z")), ] }; pub(super) const PRIMITIVE_DATE_TIME_T_SEPARATED: &[BorrowedFormatItem<'_>] = { &[ YEAR, - StringLiteral("-"), + Literal(b"-"), MONTH, - StringLiteral("-"), + Literal(b"-"), DAY, - StringLiteral("T"), + Literal(b"T"), HOUR, - StringLiteral(":"), + Literal(b":"), MINUTE, - Optional(&StringLiteral(":")), + Optional(&Literal(b":")), SECOND, - Optional(&StringLiteral(".")), + Optional(&Literal(b".")), SUBSECOND, - Optional(&StringLiteral("Z")), + Optional(&Literal(b"Z")), ] }; } From 54706f839c7966f1c361121ac4d25563770c4859 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 19:41:22 +0300 Subject: [PATCH 11/16] style: apply cargo fmt to time.rs --- sqlx-sqlite/src/types/time.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index 55e38c26df..b6826cb5b1 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -1,14 +1,14 @@ use crate::arguments::SqliteArgumentsBuffer; use crate::value::ValueRef; use crate::{ - Sqlite, SqliteTypeInfo, SqliteValueRef, decode::Decode, encode::{Encode, IsNull}, error::BoxDynError, type_info::DataType, types::Type, + Sqlite, SqliteTypeInfo, SqliteValueRef, }; -use time::format_description::{BorrowedFormatItem, well_known::Rfc3339}; +use time::format_description::{well_known::Rfc3339, BorrowedFormatItem}; use time::macros::format_description as fd; use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; @@ -195,7 +195,7 @@ fn decode_datetime_from_text(value: &str) -> Option { #[allow(deprecated)] mod formats { use time::format_description::BorrowedFormatItem::{Component, Literal, Optional}; - use time::format_description::{BorrowedFormatItem, Component::*, modifier}; + use time::format_description::{modifier, BorrowedFormatItem, Component::*}; const YEAR: BorrowedFormatItem<'_> = Component(Year({ let mut value = modifier::Year::default(); From f670c00d2a15bb1718e4411171a9826499145875 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 19:51:57 +0300 Subject: [PATCH 12/16] test(sqlite): rename test table to packages per reviewer feedback --- tests/sqlite/macros.rs | 13 +++++-------- tests/sqlite/setup.sql | 11 +++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index 1c90a1029f..42384bfbc3 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -379,14 +379,11 @@ async fn test_returning_primary_key_is_not_nullable() -> anyhow::Result<()> { async fn test_returning_with_foreign_key_is_not_nullable() -> anyhow::Result<()> { let mut conn = new::().await?; - sqlx::query("INSERT INTO projects ( project_id ) VALUES ( 1 )") - .execute(&mut conn) - .await?; - - let _id: i64 = - sqlx::query_scalar!(r#"INSERT INTO foo ( project_id ) VALUES ( 1 ) RETURNING package_id"#) - .fetch_one(&mut conn) - .await?; + let _id: i64 = sqlx::query_scalar!( + r#"INSERT INTO packages ( project_id ) VALUES ( 1 ) RETURNING package_id"# + ) + .fetch_one(&mut conn) + .await?; Ok(()) } diff --git a/tests/sqlite/setup.sql b/tests/sqlite/setup.sql index 50eb0334de..c7efca7919 100644 --- a/tests/sqlite/setup.sql +++ b/tests/sqlite/setup.sql @@ -40,12 +40,7 @@ CREATE TABLE products ( CONSTRAINT price_greater_than_zero CHECK (price > 0) ); -CREATE TABLE projects ( - project_id INTEGER PRIMARY KEY -); - -CREATE TABLE foo ( - package_id INTEGER PRIMARY KEY NOT NULL CHECK(package_id >= 0), - project_id INTEGER NOT NULL, - FOREIGN KEY(project_id) REFERENCES projects(project_id) +CREATE TABLE packages ( + package_id INTEGER PRIMARY KEY, + project_id INTEGER ); From eaaa1fe5f4d3739e5af22c65e24aa76c76c0fda7 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 20:00:18 +0300 Subject: [PATCH 13/16] ci: retrigger From 00ca3b616acf3b43ad0de6c9d416acae1fdd36bf Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 20:15:44 +0300 Subject: [PATCH 14/16] chore: restore lost comment in macros.rs --- tests/sqlite/macros.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs index 42384bfbc3..cca0b19593 100644 --- a/tests/sqlite/macros.rs +++ b/tests/sqlite/macros.rs @@ -358,6 +358,8 @@ async fn test_column_override_exact_nullable() -> anyhow::Result<()> { Ok(()) } +// we don't emit bind parameter typechecks for SQLite so testing the overrides is redundant + // Regression test: INTEGER PRIMARY KEY (a rowid alias) must not be nullable. // Before the fix, `query!` would infer `id` as `Option` and the // assignment `let _: i64 = row.id` would fail to compile. From 64b83e56e8761a60cb00f2fff78047b1fe4d6ae2 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 23:11:53 +0300 Subject: [PATCH 15/16] test(sqlite): update sqlite.db with new regression test schemas --- tests/sqlite/sqlite.db | Bin 45056 -> 40960 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/sqlite/sqlite.db b/tests/sqlite/sqlite.db index 606bdcda3e5298329833c27a4421dc67186405d3..5d776ba266e41d935948ff4062625923f049d562 100644 GIT binary patch delta 167 zcmZp8z|?SnX@ayO7Xt$W2*Ut7kii1wfW#RmHY&?=G3XU7;pP9qz{wrYz^}xAmiIH? zYupXSbm$>6M8wxz+GAZO_7nhY~Y&0!NOv*_uNKDR7OiwL_(Ok|!u8twD3L%b8 rKCTKd(aD#%R2g+9ujLlse1SWHWwWlpW&Vi)teX^A3O2J8{NV=x#-uCu delta 374 zcmZoTz|`=7X@ayYF9QPuHxR=B2av%6<#10{5Kvd-Wzg&W&CCCTfs1Pm1HTggS>Dfl zuX)pXuJhD!U*az3TC=fHhfA%|gNt2USeUV?yd*IxCp9fUABeb}gIpa$TopnboqSvs zfC7^zbF0=X`1yw@_=WoTC^&n#I(usrBqnDkrl-bdrYP9iDi~<$a47&mK~a8IYH|rk z)YC7-)!j7+t`DNX%|FQ1)7?+O+ci=Hp;%KP$koj?$kor;H5g__F|s_Dvt}eGySS_@ zVdoE^K From 6d1f84b9a19ead7200346678a1ffaa669b719b61 Mon Sep 17 00:00:00 2001 From: at264939-ctrl Date: Sat, 27 Jun 2026 23:24:51 +0300 Subject: [PATCH 16/16] fix(sqlite): restore Optional wrapper for time formats and regenerate clean db --- sqlx-sqlite/src/types/time.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index b6826cb5b1..950f51544a 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -271,12 +271,12 @@ mod formats { Literal(b":"), MINUTE, Optional(&Literal(b":")), - SECOND, + Optional(&SECOND), Optional(&Literal(b".")), - SUBSECOND, - OFFSET_HOUR, + Optional(&SUBSECOND), + Optional(&OFFSET_HOUR), Optional(&Literal(b":")), - OFFSET_MINUTE, + Optional(&OFFSET_MINUTE), ] }; @@ -292,9 +292,9 @@ mod formats { Literal(b":"), MINUTE, Optional(&Literal(b":")), - SECOND, + Optional(&SECOND), Optional(&Literal(b".")), - SUBSECOND, + Optional(&SUBSECOND), Optional(&Literal(b"Z")), ] }; @@ -311,9 +311,9 @@ mod formats { Literal(b":"), MINUTE, Optional(&Literal(b":")), - SECOND, + Optional(&SECOND), Optional(&Literal(b".")), - SUBSECOND, + Optional(&SUBSECOND), Optional(&Literal(b"Z")), ] };