diff --git a/Cargo.toml b/Cargo.toml index 6bad519a..d8f78184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,11 @@ name = "sqlite-describe" path = "tests/sqlite/describe.rs" required-features = [ "sqlite" ] +[[test]] +name = "sqlite-macros" +path = "tests/sqlite/macros.rs" +required-features = [ "sqlite", "macros" ] + # # MySQL # @@ -127,6 +132,11 @@ name = "mysql-describe" path = "tests/mysql/describe.rs" required-features = [ "mysql" ] +[[test]] +name = "mysql-macros" +path = "tests/mysql/macros.rs" +required-features = [ "mysql", "macros" ] + # # PostgreSQL # @@ -146,6 +156,11 @@ name = "postgres-describe" path = "tests/postgres/describe.rs" required-features = [ "postgres" ] +[[test]] +name = "postgres-macros" +path = "tests/postgres/macros.rs" +required-features = [ "postgres", "macros" ] + # # Microsoft SQL Server (MSSQL) # diff --git a/sqlx-core/src/mysql/connection/executor.rs b/sqlx-core/src/mysql/connection/executor.rs index b0042062..6530ab9a 100644 --- a/sqlx-core/src/mysql/connection/executor.rs +++ b/sqlx-core/src/mysql/connection/executor.rs @@ -275,9 +275,10 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection { for _ in 0..(ok.columns as usize) { let def: ColumnDefinition = self.stream.recv().await?; let ty = MySqlTypeInfo::from_column(&def); + let name = def.name()?; columns.push(Column { - name: def.name()?.to_owned(), + name: if name.is_empty() { def.alias()? } else { name }.to_owned(), type_info: ty, not_null: Some(def.flags.contains(ColumnFlags::NOT_NULL)), }) diff --git a/sqlx-core/src/mysql/type_info.rs b/sqlx-core/src/mysql/type_info.rs index a37e5bd9..75153e84 100644 --- a/sqlx-core/src/mysql/type_info.rs +++ b/sqlx-core/src/mysql/type_info.rs @@ -78,6 +78,18 @@ impl PartialEq for MySqlTypeInfo { == other.flags.contains(ColumnFlags::UNSIGNED); } + // for string types, check that our charset matches + ColumnType::VarChar + | ColumnType::Blob + | ColumnType::TinyBlob + | ColumnType::MediumBlob + | ColumnType::LongBlob + | ColumnType::String + | ColumnType::VarString + | ColumnType::Enum => { + return self.char_set == other.char_set; + } + _ => {} } diff --git a/sqlx-core/src/mysql/types/str.rs b/sqlx-core/src/mysql/types/str.rs index 86210901..17a6464e 100644 --- a/sqlx-core/src/mysql/types/str.rs +++ b/sqlx-core/src/mysql/types/str.rs @@ -36,7 +36,7 @@ impl<'r> Decode<'r, MySql> for &'r str { | ColumnType::String | ColumnType::VarString | ColumnType::Enum - ) + ) && ty.char_set == 224 } fn decode(value: MySqlValueRef<'r>) -> Result { diff --git a/sqlx-macros/src/database/mysql.rs b/sqlx-macros/src/database/mysql.rs index e6b04d48..43c5291e 100644 --- a/sqlx-macros/src/database/mysql.rs +++ b/sqlx-macros/src/database/mysql.rs @@ -13,6 +13,7 @@ impl_database_ext! { f32, f64, + // ordering is important here as otherwise we might infer strings to be binary // CHAR, VAR_CHAR, TEXT String, diff --git a/sqlx-macros/src/query/data.rs b/sqlx-macros/src/query/data.rs index 3f7e4183..9af35a48 100644 --- a/sqlx-macros/src/query/data.rs +++ b/sqlx-macros/src/query/data.rs @@ -10,6 +10,7 @@ use sqlx_core::executor::Executor; deserialize = "Describe: serde::de::DeserializeOwned" )) )] +#[derive(Debug)] pub struct QueryData { #[allow(dead_code)] pub(super) query: String, diff --git a/sqlx-macros/src/query/mod.rs b/sqlx-macros/src/query/mod.rs index 1300b024..2daed064 100644 --- a/sqlx-macros/src/query/mod.rs +++ b/sqlx-macros/src/query/mod.rs @@ -183,7 +183,7 @@ where let sql = &input.src; quote! { - sqlx::query::<#db_path>(#sql).bind_all(#query_args) + sqlx::query_with::<#db_path, _>(#sql, #query_args) } } else { let columns = output::columns_to_rust::(&data.describe)?; diff --git a/sqlx-macros/src/query/output.rs b/sqlx-macros/src/query/output.rs index 09c78cd9..cdc6c738 100644 --- a/sqlx-macros/src/query/output.rs +++ b/sqlx-macros/src/query/output.rs @@ -119,7 +119,7 @@ pub fn quote_query_as( let sql = &input.src; quote! { - sqlx::query_with::<#db_path>(#sql, #bind_args).try_map(|row: #row_path| { + sqlx::query_with::<#db_path, _>(#sql, #bind_args).try_map(|row: #row_path| { use sqlx::Row as _; use sqlx::result_ext::ResultExt as _; diff --git a/tests/mysql/macros.rs b/tests/mysql/macros.rs new file mode 100644 index 00000000..f949fb48 --- /dev/null +++ b/tests/mysql/macros.rs @@ -0,0 +1,94 @@ +use sqlx::MySql; +use sqlx_test::new; + +#[sqlx_macros::test] +async fn macro_select_from_cte() -> anyhow::Result<()> { + let mut conn = new::().await?; + let account = + sqlx::query!("select * from (select (1) as id, 'Herp Derpinson' as name, cast(null as char) email) accounts") + .fetch_one(&mut conn) + .await?; + + assert_eq!(account.id, 1); + assert_eq!(account.name, "Herp Derpinson"); + // MySQL can tell us the nullability of expressions, ain't that cool + assert_eq!(account.email, None); + + Ok(()) +} + +#[sqlx_macros::test] +async fn macro_select_from_cte_bind() -> anyhow::Result<()> { + let mut conn = new::().await?; + let account = sqlx::query!( + "select * from (select (1) as id, 'Herp Derpinson' as name) 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, +} + +#[sqlx_macros::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!(account.name, None); + assert_eq!(account.r#type, 1); + + println!("{:?}", account); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_query_as_bool() -> anyhow::Result<()> { + let mut conn = new::().await?; + + struct Article { + id: i32, + deleted: bool, + } + + let article = sqlx::query_as_unchecked!( + Article, + "select * from (select 51 as id, true as deleted) articles" + ) + .fetch_one(&mut conn) + .await?; + + assert_eq!(51, article.id); + assert_eq!(true, article.deleted); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_query_bytes() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let rec = sqlx::query!("SELECT X'01AF' as _1") + .fetch_one(&mut conn) + .await?; + + assert_eq!(rec._1, &[0x01_u8, 0xAF_u8]); + + Ok(()) +} diff --git a/tests/postgres/macros.rs b/tests/postgres/macros.rs index f0005d0a..37d5019a 100644 --- a/tests/postgres/macros.rs +++ b/tests/postgres/macros.rs @@ -65,7 +65,8 @@ async fn test_text_var_char_char_n() -> anyhow::Result<()> { async fn _file() -> anyhow::Result<()> { let mut conn = new::().await?; - let account = sqlx::query_file!("tests/test-query.sql",) + // keep trailing comma as a test + let account = sqlx::query_file!("tests/postgres/test-query.sql",) .fetch_one(&mut conn) .await?; @@ -129,7 +130,7 @@ async fn test_query_as_raw() -> anyhow::Result<()> { async fn test_query_file_as() -> anyhow::Result<()> { let mut conn = new::().await?; - let account = sqlx::query_file_as!(Account, "tests/test-query.sql",) + let account = sqlx::query_file_as!(Account, "tests/postgres/test-query.sql",) .fetch_one(&mut conn) .await?; diff --git a/tests/postgres/test-query.sql b/tests/postgres/test-query.sql new file mode 100644 index 00000000..cb8ab010 --- /dev/null +++ b/tests/postgres/test-query.sql @@ -0,0 +1 @@ +SELECT * from (VALUES (1, null)) accounts(id, name) diff --git a/tests/sqlite/macros.rs b/tests/sqlite/macros.rs new file mode 100644 index 00000000..2e0825c9 --- /dev/null +++ b/tests/sqlite/macros.rs @@ -0,0 +1,73 @@ +use sqlx::Sqlite; +use sqlx_test::new; + +#[sqlx_macros::test] +async fn macro_select() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let account = sqlx::query!("select id, name, is_active from accounts where id = 1") + .fetch_one(&mut conn) + .await?; + + assert_eq!(1, account.id); + assert_eq!("Herp Derpinson", account.name); + assert_eq!(account.is_active, None); + + Ok(()) +} + +#[sqlx_macros::test] +async fn macro_select_bind() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let account = sqlx::query!( + "select id, name, is_active from accounts where id = ?", + 1i32 + ) + .fetch_one(&mut conn) + .await?; + + assert_eq!(1, account.id); + assert_eq!("Herp Derpinson", account.name); + assert_eq!(account.is_active, None); + + Ok(()) +} + +#[derive(Debug)] +struct RawAccount { + id: i32, + name: String, + is_active: Option, +} + +#[sqlx_macros::test] +async fn test_query_as_raw() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let account = sqlx::query_as!(RawAccount, "SELECT id, name, is_active from accounts") + .fetch_one(&mut conn) + .await?; + + assert_eq!(account.id, 1); + assert_eq!(account.name, "Herp Derpinson"); + assert_eq!(account.is_active, None); + + Ok(()) +} + +#[sqlx_macros::test] +async fn macro_select_from_view() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let account = sqlx::query!("SELECT id, name, is_active from accounts_view") + .fetch_one(&mut conn) + .await?; + + // SQLite tells us the true origin of these columns even through the view + assert_eq!(account.id, 1); + assert_eq!(account.name, "Herp Derpinson"); + assert_eq!(account.is_active, None); + + Ok(()) +} diff --git a/tests/sqlite/sqlite.db b/tests/sqlite/sqlite.db index 8262b8a3..9faa9c8e 100644 Binary files a/tests/sqlite/sqlite.db and b/tests/sqlite/sqlite.db differ