Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
dea4fce
fix(sqlite): handle nullability of INTEGER PRIMARY KEY in RETURNING
EbrahimEldesoky Jun 24, 2026
7e78dc7
chore: allow deprecated time types to fix CI
EbrahimEldesoky Jun 24, 2026
15865b0
refactor: address review comments (add regression test and update docs)
EbrahimEldesoky Jun 27, 2026
df39d72
test(sqlite): make regression test robust against CI interference
EbrahimEldesoky Jun 27, 2026
1b39bc2
chore: restore comment and remove redundant deprecation allow
EbrahimEldesoky Jun 27, 2026
bd76520
fix: update time crate formats to non-deprecated API (0.3.48+)
EbrahimEldesoky Jun 27, 2026
1c37c77
fix: update time crate formats to non-deprecated API (0.3.48+)
EbrahimEldesoky Jun 27, 2026
7a6a3ae
fix(sqlite): resolve INTEGER PRIMARY KEY nullability regression in RE…
EbrahimEldesoky Jun 27, 2026
4b22b6c
style: apply cargo fmt
EbrahimEldesoky Jun 27, 2026
6f8a8b4
fix(sqlite): use #[allow(deprecated)] for time format compatibility
EbrahimEldesoky Jun 27, 2026
54706f8
style: apply cargo fmt to time.rs
EbrahimEldesoky Jun 27, 2026
f670c00
test(sqlite): rename test table to packages per reviewer feedback
EbrahimEldesoky Jun 27, 2026
eaaa1fe
ci: retrigger
EbrahimEldesoky Jun 27, 2026
00ca3b6
chore: restore lost comment in macros.rs
EbrahimEldesoky Jun 27, 2026
64b83e5
test(sqlite): update sqlite.db with new regression test schemas
EbrahimEldesoky Jun 27, 2026
6d1f84b
fix(sqlite): restore Optional wrapper for time formats and regenerate…
EbrahimEldesoky Jun 27, 2026
f34a3aa
Merge remote-tracking branch 'origin/main' into fix/sqlite-returning-…
EbrahimEldesoky Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions sqlx-sqlite/src/connection/describe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));

Expand Down
16 changes: 8 additions & 8 deletions sqlx-sqlite/src/connection/explain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,17 +373,17 @@ fn opcode_to_type(op: &str) -> DataType {
fn root_block_columns(
conn: &mut ConnectionState,
) -> Result<HashMap<(i64, i64), IntMap<ColumnType>>, 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
) s
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
Expand All @@ -400,13 +400,13 @@ fn root_block_columns(
.collect::<Result<Vec<_>, Error>>()?;

let mut row_info: HashMap<(i64, i64), IntMap<ColumnType>> = 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"))),
Comment thread
EbrahimEldesoky marked this conversation as resolved.
},
);
}
Expand Down Expand Up @@ -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)
);
Expand All @@ -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)
);
Expand All @@ -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)
);
Expand Down
6 changes: 3 additions & 3 deletions sqlx-sqlite/src/statement/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<bool>, Error> {
unsafe {
Expand Down Expand Up @@ -271,7 +271,7 @@ impl StatementHandle {
.to_bytes()
.eq_ignore_ascii_case("integer".as_bytes())
{
None
Some(false)
Comment thread
EbrahimEldesoky marked this conversation as resolved.
} else {
Some(not_null == 0)
},
Expand Down
30 changes: 30 additions & 0 deletions tests/sqlite/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,33 @@ async fn test_column_override_exact_nullable() -> anyhow::Result<()> {
}

// we don't emit bind parameter typechecks for SQLite so testing the overrides is redundant
Comment thread
EbrahimEldesoky marked this conversation as resolved.
Comment thread
EbrahimEldesoky marked this conversation as resolved.

// Regression test: INTEGER PRIMARY KEY (a rowid alias) must not be nullable.
// Before the fix, `query!` would infer `id` as `Option<i64>` 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::<Sqlite>().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(())
}

#[sqlx_macros::test]
async fn test_returning_with_foreign_key_is_not_nullable() -> anyhow::Result<()> {
let mut conn = new::<Sqlite>().await?;

let _id: i64 = sqlx::query_scalar!(
r#"INSERT INTO packages ( project_id ) VALUES ( 1 ) RETURNING package_id"#
)
.fetch_one(&mut conn)
.await?;

Ok(())
}
9 changes: 9 additions & 0 deletions tests/sqlite/setup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,3 +39,8 @@ CREATE TABLE products (
price NUMERIC,
CONSTRAINT price_greater_than_zero CHECK (price > 0)
);

CREATE TABLE packages (
package_id INTEGER PRIMARY KEY,
project_id INTEGER
);
Binary file modified tests/sqlite/sqlite.db
Binary file not shown.
Loading