From 60c6df5703ce1b51569fade54a5f7587b19c8cf2 Mon Sep 17 00:00:00 2001 From: Scott Driggers Date: Tue, 11 Aug 2026 09:21:34 -0400 Subject: [PATCH] fix(sqlite): don't dereference a NULL declared type in column_nullable sqlite3_table_column_metadata() leaves its declared-type out-param NULL for a column declared without a type, e.g. `CREATE TABLE foo (bar PRIMARY KEY)`. column_nullable() passed that pointer straight to CStr::from_ptr(), so describing such a column segfaulted the process, and rustc itself, since the query!() macros describe a live database at expansion time. A column with no declared type is not declared INTEGER and so cannot be a rowid alias; treat it as a normal column and fall through to the NOT NULL flag. Regression introduced in 69ee0df (#4088), released in 0.9.0. --- sqlx-sqlite/src/statement/handle.rs | 27 +++++++++++----------- tests/sqlite/describe.rs | 35 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index c78ce98414..98f3a483d0 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -263,19 +263,20 @@ impl StatementHandle { return Err(SqliteError::new(self.db_handle()).into()); } - let datatype = CStr::from_ptr(datatype); - - Ok( - if primary_key != 0 - && datatype - .to_bytes() - .eq_ignore_ascii_case("integer".as_bytes()) - { - None - } else { - Some(not_null == 0) - }, - ) + // `sqlite3_table_column_metadata()` sets the declared type to NULL for a column + // declared without one, e.g. `CREATE TABLE foo (bar PRIMARY KEY)`. Such a column + // has no type name to compare against, and is by definition not declared + // `INTEGER`, so it cannot be a rowid alias. + let is_integer = !datatype.is_null() + && CStr::from_ptr(datatype) + .to_bytes() + .eq_ignore_ascii_case("integer".as_bytes()); + + Ok(if primary_key != 0 && is_integer { + None + } else { + Some(not_null == 0) + }) } } diff --git a/tests/sqlite/describe.rs b/tests/sqlite/describe.rs index 49bdbd35e7..f303f7509d 100644 --- a/tests/sqlite/describe.rs +++ b/tests/sqlite/describe.rs @@ -2,7 +2,7 @@ use sqlx::error::DatabaseError; use sqlx::sqlite::{SqliteConnectOptions, SqliteError}; use sqlx::TypeInfo; use sqlx::{sqlite::Sqlite, Column, Executor}; -use sqlx::{ConnectOptions, SqlSafeStr}; +use sqlx::{ConnectOptions, Connection, SqlSafeStr, SqliteConnection}; use sqlx_test::new; use std::env; @@ -1095,3 +1095,36 @@ async fn it_describes_analytical_function() -> anyhow::Result<()> { Ok(()) } + +// A column declared with no type at all, e.g. `CREATE TABLE foo (bar PRIMARY KEY)`, is +// legal SQLite. `sqlite3_table_column_metadata()` reports a NULL declared type for such a +// column, which `column_nullable()` used to dereference unconditionally, segfaulting the +// process, and thus rustc itself when the `query!()` macros describe a live database. +#[sqlx_macros::test] +async fn it_describes_columns_with_no_declared_type() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect(":memory:").await?; + + conn.execute( + r#" + CREATE TABLE untyped ( + id PRIMARY KEY, + typeless, + typed TEXT NOT NULL + ); + "# + .into_sql_str(), + ) + .await?; + + let d = conn + .describe("SELECT id, typeless, typed FROM untyped".into_sql_str()) + .await?; + + // A `PRIMARY KEY` with no declared type is not an `INTEGER PRIMARY KEY`, so it is not a + // rowid alias and SQLite does permit NULLs in it. + assert_eq!(d.nullable(0), Some(true)); + assert_eq!(d.nullable(1), Some(true)); + assert_eq!(d.nullable(2), Some(false)); + + Ok(()) +}