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(()) +}