From 8328e07c97af64e3b45e308c0bcf982dd7e85ad5 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Mon, 16 Mar 2020 20:55:19 -0700 Subject: [PATCH] macros + sqlite: fix error for null-typed columns --- sqlx-core/src/mysql/types/mod.rs | 4 ++ sqlx-core/src/postgres/types/mod.rs | 5 ++ sqlx-core/src/sqlite/types/mod.rs | 4 ++ sqlx-core/src/types.rs | 8 +++ sqlx-macros/src/query_macros/output.rs | 10 ++++ tests/sqlite-macros.rs | 61 +++++++++++++++++++++++ tests/ui-tests.rs | 4 ++ tests/ui/sqlite/expression-column-type.rs | 3 ++ 8 files changed, 99 insertions(+) create mode 100644 tests/sqlite-macros.rs create mode 100644 tests/ui/sqlite/expression-column-type.rs diff --git a/sqlx-core/src/mysql/types/mod.rs b/sqlx-core/src/mysql/types/mod.rs index 2c4421f1..6cf9ec20 100644 --- a/sqlx-core/src/mysql/types/mod.rs +++ b/sqlx-core/src/mysql/types/mod.rs @@ -106,6 +106,10 @@ impl TypeInfo for MySqlTypeInfo { _ => self.id.0 == other.id.0 && self.is_unsigned == other.is_unsigned, } } + + fn is_null_type(&self) -> bool { + self.id == TypeId::NULL + } } impl<'de, T> Decode<'de, MySql> for Option diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs index ab202c40..5491e85c 100644 --- a/sqlx-core/src/postgres/types/mod.rs +++ b/sqlx-core/src/postgres/types/mod.rs @@ -81,6 +81,11 @@ impl TypeInfo for PgTypeInfo { // TODO: 99% of postgres types are direct equality for [compatible]; when we add something that isn't (e.g, JSON/JSONB), fix this here self.id.0 == other.id.0 } + + fn is_null_type(&self) -> bool { + // Postgres doesn't have a "null" type + false + } } impl<'de, T> Decode<'de, Postgres> for Option diff --git a/sqlx-core/src/sqlite/types/mod.rs b/sqlx-core/src/sqlite/types/mod.rs index dc1eb3d8..50475af0 100644 --- a/sqlx-core/src/sqlite/types/mod.rs +++ b/sqlx-core/src/sqlite/types/mod.rs @@ -66,6 +66,10 @@ impl TypeInfo for SqliteTypeInfo { fn compatible(&self, other: &Self) -> bool { self.affinity == other.affinity } + + fn is_null_type(&self) -> bool { + self.r#type == SqliteType::Null + } } impl<'de, T> Decode<'de, Sqlite> for Option diff --git a/sqlx-core/src/types.rs b/sqlx-core/src/types.rs index 1425acd2..4df0dbdf 100644 --- a/sqlx-core/src/types.rs +++ b/sqlx-core/src/types.rs @@ -18,6 +18,14 @@ pub trait TypeInfo: Debug + Display + Clone { /// Compares type information to determine if `other` is compatible at the Rust level /// with `self`. fn compatible(&self, other: &Self) -> bool; + + /// Return `true` if this is the database flavor's "null" sentinel type. + /// + /// For type info coming from the description of a prepared statement, this means + /// that the server could not infer the expected type of a bind parameter or result column; + /// the latter is most often the case with columns that are the result of an expression + /// in a weakly-typed database like MySQL or SQLite. + fn is_null_type(&self) -> bool; } /// Indicates that a SQL type is supported for a database. diff --git a/sqlx-macros/src/query_macros/output.rs b/sqlx-macros/src/query_macros/output.rs index 5a1d17fb..4bc8eb8f 100644 --- a/sqlx-macros/src/query_macros/output.rs +++ b/sqlx-macros/src/query_macros/output.rs @@ -3,6 +3,7 @@ use quote::quote; use syn::Path; use sqlx::describe::Describe; +use sqlx::types::TypeInfo; use crate::database::DatabaseExt; @@ -58,6 +59,15 @@ pub fn columns_to_rust(describe: &Describe) -> crate::Resul name: column.name.as_deref() } ) + } else if column.type_info.is_null_type() { + format!( + "database couldn't tell us the type of {col}; \ + this can happen for columns that are the result of an expression", + col = DisplayColumn { + idx: i, + name: column.name.as_deref() + } + ) } else { format!( "unsupported type {ty} of {col}", diff --git a/tests/sqlite-macros.rs b/tests/sqlite-macros.rs new file mode 100644 index 00000000..ef9275ac --- /dev/null +++ b/tests/sqlite-macros.rs @@ -0,0 +1,61 @@ +use sqlx::Sqlite; +use sqlx_test::new; + +#[cfg_attr(feature = "runtime-async-std", async_std::test)] +#[cfg_attr(feature = "runtime-tokio", tokio::test)] +async fn macro_select_from_cte() -> anyhow::Result<()> { + let mut conn = new::().await?; + let account = sqlx::query!( + "with accounts(id, name) as (values (1, 'Herp Derpinson')) select * from accounts" + ) + .fetch_one(&mut conn) + .await?; + + println!("{:?}", account); + println!("{}: {}", account.id, account.name); + + Ok(()) +} + +#[cfg_attr(feature = "runtime-async-std", async_std::test)] +#[cfg_attr(feature = "runtime-tokio", tokio::test)] +async fn macro_select_from_cte_bind() -> anyhow::Result<()> { + let mut conn = new::().await?; + let account = sqlx::query!( + "with accounts(id, name) as (select 1, 'Herp Derpinson') select * from accounts where id = ?", + 1i32 + ) + .fetch_one(&mut conn) + .await?; + + println!("{:?}", account); + println!("{}: {}", account.id, account.name); + + Ok(()) +} + +#[derive(Debug)] +struct RawAccount { + r#type: i32, + name: Option, +} + +#[cfg_attr(feature = "runtime-async-std", async_std::test)] +#[cfg_attr(feature = "runtime-tokio", tokio::test)] +async fn test_query_as_raw() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let account = sqlx::query_as!( + RawAccount, + "SELECT * from (select 1 as type, cast(null as char) as name) accounts" + ) + .fetch_one(&mut conn) + .await?; + + assert_eq!(None, account.name); + assert_eq!(1, account.r#type); + + println!("{:?}", account); + + Ok(()) +} diff --git a/tests/ui-tests.rs b/tests/ui-tests.rs index a7b327ff..b83ba0f6 100644 --- a/tests/ui-tests.rs +++ b/tests/ui-tests.rs @@ -24,5 +24,9 @@ fn ui_tests() { } } + if cfg!(feature = "sqlite") { + t.compile_fail("tests/ui/sqlite/*.rs"); + } + t.compile_fail("tests/ui/*.rs"); } diff --git a/tests/ui/sqlite/expression-column-type.rs b/tests/ui/sqlite/expression-column-type.rs new file mode 100644 index 00000000..f647bd77 --- /dev/null +++ b/tests/ui/sqlite/expression-column-type.rs @@ -0,0 +1,3 @@ +fn main() { + let _ = sqlx::query!("select 1 as id"); +}