From 4163388298469bdad237b375a7807daf95f0aba1 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Fri, 24 Jan 2020 20:24:39 -0800 Subject: [PATCH] add nullability info to Describe implement nullability check for Postgres as a query on pg_attribute implement type name fetching for Postgres as part of `describe()` add nullability for describe() to MySQL improve errors with unknown result column type IDs in `query!()` run cargo fmt and fix warnings improve error when feature gates for chrono/uuid types is not turned on workflows/rust: add step to UI-test missing optional features improve error for unsupported/feature-gated input parameter types fix `PgConnection::get_type_names()` for empty type IDs list fix `tests::mysql::test_describe()` on MariaDB 10.4 copy-edit unsupported/feature-gated type errors in `query!()` Postgres: fix SQL type of string array closes #107 closes #17 Co-Authored-By: Anthony Dodd --- .github/workflows/rust.yml | 35 ++++ sqlx-core/src/describe.rs | 3 + sqlx-core/src/mysql/executor.rs | 6 +- sqlx-core/src/mysql/protocol/type.rs | 29 +++- sqlx-core/src/mysql/types/mod.rs | 20 ++- sqlx-core/src/postgres/cursor.rs | 4 +- sqlx-core/src/postgres/executor.rs | 163 +++++++++++++++++-- sqlx-core/src/postgres/protocol/statement.rs | 7 +- sqlx-core/src/postgres/protocol/type_id.rs | 2 +- sqlx-core/src/postgres/types/bool.rs | 4 +- sqlx-core/src/postgres/types/bytes.rs | 4 +- sqlx-core/src/postgres/types/chrono.rs | 16 +- sqlx-core/src/postgres/types/float.rs | 8 +- sqlx-core/src/postgres/types/int.rs | 12 +- sqlx-core/src/postgres/types/mod.rs | 79 ++++++++- sqlx-core/src/postgres/types/str.rs | 4 +- sqlx-core/src/postgres/types/uuid.rs | 4 +- sqlx-macros/src/database/mod.rs | 15 +- sqlx-macros/src/database/mysql.rs | 1 + sqlx-macros/src/database/postgres.rs | 1 + sqlx-macros/src/query_macros/args.rs | 17 +- sqlx-macros/src/query_macros/output.rs | 45 ++++- tests/mysql.rs | 43 +++++ tests/postgres.rs | 33 ++++ tests/ui-tests.rs | 14 ++ tests/ui/mysql/gated/chrono.rs | 7 + tests/ui/mysql/gated/chrono.stderr | 23 +++ tests/ui/postgres/gated/chrono.rs | 17 ++ tests/ui/postgres/gated/chrono.stderr | 63 +++++++ tests/ui/postgres/gated/uuid.rs | 4 + tests/ui/postgres/gated/uuid.stderr | 15 ++ tests/ui/postgres/unsupported-type.rs | 5 + tests/ui/postgres/unsupported-type.stderr | 15 ++ 33 files changed, 654 insertions(+), 64 deletions(-) create mode 100644 tests/ui/mysql/gated/chrono.rs create mode 100644 tests/ui/mysql/gated/chrono.stderr create mode 100644 tests/ui/postgres/gated/chrono.rs create mode 100644 tests/ui/postgres/gated/chrono.stderr create mode 100644 tests/ui/postgres/gated/uuid.rs create mode 100644 tests/ui/postgres/gated/uuid.stderr create mode 100644 tests/ui/postgres/unsupported-type.rs create mode 100644 tests/ui/postgres/unsupported-type.stderr diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ab6da8131..f10b39eb3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -120,6 +120,16 @@ jobs: env: DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres + # UI feature gate tests: async-std + - run: cargo test --no-default-features --features 'runtime-async-std postgres macros tls' + env: + DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres + + # UI feature gate tests: tokio + - run: cargo test --no-default-features --features 'runtime-tokio postgres macros tls' + env: + DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres + mysql: needs: build runs-on: ubuntu-latest @@ -176,6 +186,21 @@ jobs: # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem + # UI feature gate tests: async-std + - run: cargo test --no-default-features --features 'runtime-async-std mysql macros tls' --test ui-tests + env: + # pass the path to the CA that the MySQL service generated + # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly + DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem + + # UI feature gate tests: tokio + - run: cargo test --no-default-features --features 'runtime-tokio mysql macros tls' --test ui-tests + env: + # pass the path to the CA that the MySQL service generated + # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly + DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem + + mariadb: needs: build runs-on: ubuntu-latest @@ -225,3 +250,13 @@ jobs: - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono tls' env: DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx + + # UI feature gate tests: async-std + - run: cargo test --no-default-features --features 'runtime-async-std mysql macros tls' + env: + DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx + + # UI feature gate tests: tokio + - run: cargo test --no-default-features --features 'runtime-tokio mysql macros tls' + env: + DATABASE_URL: mariadb://root:password@localhost:${{ job.services.mariadb.ports[3306] }}/sqlx diff --git a/sqlx-core/src/describe.rs b/sqlx-core/src/describe.rs index 544bf8bc1..4eb3de225 100644 --- a/sqlx-core/src/describe.rs +++ b/sqlx-core/src/describe.rs @@ -40,6 +40,8 @@ where pub name: Option>, pub table_id: Option, pub type_info: DB::TypeInfo, + /// Whether or not the column cannot be `NULL` (or if that is even knowable). + pub non_null: Option, } impl Debug for Column @@ -53,6 +55,7 @@ where .field("name", &self.name) .field("table_id", &self.table_id) .field("type_id", &self.type_info) + .field("nonnull", &self.non_null) .finish() } } diff --git a/sqlx-core/src/mysql/executor.rs b/sqlx-core/src/mysql/executor.rs index cb75779c0..de92ac07e 100644 --- a/sqlx-core/src/mysql/executor.rs +++ b/sqlx-core/src/mysql/executor.rs @@ -4,11 +4,11 @@ use std::sync::Arc; use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; -use crate::describe::{Column, Describe}; +use crate::describe::{Column, Describe, Nullability}; use crate::executor::Executor; use crate::mysql::protocol::{ Capabilities, ColumnCount, ColumnDefinition, ComQuery, ComStmtExecute, ComStmtPrepare, - ComStmtPrepareOk, Cursor, Decode, EofPacket, OkPacket, Row, TypeId, + ComStmtPrepareOk, Cursor, Decode, EofPacket, FieldFlags, OkPacket, Row, TypeId, }; use crate::mysql::{MySql, MySqlArguments, MySqlConnection, MySqlRow, MySqlTypeInfo}; @@ -253,10 +253,12 @@ impl MySqlConnection { for _ in 0..prepare_ok.columns { let column = ColumnDefinition::decode(self.receive().await?.packet())?; + result_columns.push(Column:: { type_info: MySqlTypeInfo::from_column_def(&column), name: column.column_alias.or(column.column), table_id: column.table_alias.or(column.table), + non_null: Some(column.flags.contains(FieldFlags::NOT_NULL)), }); } diff --git a/sqlx-core/src/mysql/protocol/type.rs b/sqlx-core/src/mysql/protocol/type.rs index 6284401b0..b242ed02b 100644 --- a/sqlx-core/src/mysql/protocol/type.rs +++ b/sqlx-core/src/mysql/protocol/type.rs @@ -1,10 +1,36 @@ +use std::fmt::{self, Debug, Display, Formatter}; + // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/binary__log__types_8h.html // https://mariadb.com/kb/en/library/resultset/#field-types #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TypeId(pub u8); +macro_rules! type_id_consts { + ($( + pub const $name:ident: TypeId = TypeId($id:literal); + )*) => ( + impl TypeId { + $(pub const $name: TypeId = TypeId($id);)* + + #[doc(hidden)] + pub fn type_name(&self) -> &'static str { + match self.0 { + $($id => stringify!($name),)* + _ => "" + } + } + } + ) +} + +impl Display for TypeId { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{} ({:#x})", self.type_name(), self.0) + } +} + // https://github.com/google/mysql/blob/c01fc2134d439282a21a2ddf687566e198ddee28/include/mysql_com.h#L429 -impl TypeId { +type_id_consts! { pub const NULL: TypeId = TypeId(6); // String: CHAR, VARCHAR, TEXT @@ -23,6 +49,7 @@ impl TypeId { pub const SMALL_INT: TypeId = TypeId(2); pub const INT: TypeId = TypeId(3); pub const BIG_INT: TypeId = TypeId(8); + pub const MEDIUM_INT: TypeId = TypeId(9); // Numeric: FLOAT, DOUBLE pub const FLOAT: TypeId = TypeId(4); diff --git a/sqlx-core/src/mysql/types/mod.rs b/sqlx-core/src/mysql/types/mod.rs index 39e8252b1..475370ca8 100644 --- a/sqlx-core/src/mysql/types/mod.rs +++ b/sqlx-core/src/mysql/types/mod.rs @@ -49,12 +49,28 @@ impl MySqlTypeInfo { char_set: def.char_set, } } + + #[doc(hidden)] + pub fn type_name(&self) -> &'static str { + self.id.type_name() + } + + #[doc(hidden)] + pub fn type_feature_gate(&self) -> Option<&'static str> { + match self.id { + TypeId::DATE | TypeId::TIME | TypeId::DATETIME | TypeId::TIMESTAMP => Some("chrono"), + _ => None, + } + } } impl Display for MySqlTypeInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: Should we attempt to render the type *name* here? - write!(f, "{}", self.id.0) + if self.id.type_name() != "" { + write!(f, "{}", self.id.type_name()) + } else { + write!(f, "ID {:#x}", self.id.0) + } } } diff --git a/sqlx-core/src/postgres/cursor.rs b/sqlx-core/src/postgres/cursor.rs index 7ca028ee4..08f9906c9 100644 --- a/sqlx-core/src/postgres/cursor.rs +++ b/sqlx-core/src/postgres/cursor.rs @@ -74,7 +74,7 @@ fn parse_row_description(rd: RowDescription) -> (HashMap, usize>, Vec crate::Result<(HashMap, usize>, Vec)> { let description: Option<_> = loop { @@ -108,7 +108,7 @@ async fn get_or_describe( if !conn.cache_statement_columns.contains_key(&statement) || !conn.cache_statement_formats.contains_key(&statement) { - let (columns, formats) = describe(conn).await?; + let (columns, formats) = expect_desc(conn).await?; conn.cache_statement_columns .insert(statement, Arc::new(columns)); diff --git a/sqlx-core/src/postgres/executor.rs b/sqlx-core/src/postgres/executor.rs index 220187865..c4fab9956 100644 --- a/sqlx-core/src/postgres/executor.rs +++ b/sqlx-core/src/postgres/executor.rs @@ -1,12 +1,20 @@ -use futures_core::future::BoxFuture; +use std::collections::{HashMap, HashSet}; +use std::fmt::Write; +use futures_core::future::BoxFuture; +use futures_util::{stream, StreamExt, TryStreamExt}; + +use crate::arguments::Arguments; use crate::cursor::Cursor; use crate::describe::{Column, Describe}; use crate::executor::{Execute, Executor, RefExecutor}; use crate::postgres::protocol::{ - self, CommandComplete, Message, ParameterDescription, RowDescription, StatementId, TypeFormat, + self, CommandComplete, Field, Message, ParameterDescription, RowDescription, StatementId, + TypeFormat, TypeId, }; -use crate::postgres::{PgArguments, PgConnection, PgCursor, PgTypeInfo, Postgres}; +use crate::postgres::types::SharedStr; +use crate::postgres::{PgArguments, PgConnection, PgCursor, PgRow, PgTypeInfo, Postgres}; +use crate::row::Row; impl PgConnection { pub(crate) fn write_simple_query(&mut self, query: &str) { @@ -132,13 +140,14 @@ impl PgConnection { &'e mut self, query: &'q str, ) -> crate::Result> { + self.is_ready = false; + let statement = self.write_prepare(query, &Default::default()); self.write_describe(protocol::Describe::Statement(statement)); self.write_sync(); self.stream.flush().await?; - self.wait_until_ready().await?; let params = loop { match self.stream.read().await? { @@ -171,29 +180,149 @@ impl PgConnection { } }; + self.wait_until_ready().await?; + + let result_fields = result.map_or_else(Default::default, |r| r.fields); + + // TODO: cache this result + let type_names = self + .get_type_names( + params + .ids + .iter() + .cloned() + .chain(result_fields.iter().map(|field| field.type_id)), + ) + .await?; + Ok(Describe { param_types: params .ids .iter() - .map(|id| PgTypeInfo::new(*id)) + .map(|id| PgTypeInfo::new(*id, &type_names[&id.0])) .collect::>() .into_boxed_slice(), - result_columns: result - .map(|r| r.fields) - .unwrap_or_default() - .into_vec() - .into_iter() - // TODO: Should [Column] just wrap [protocol::Field] ? - .map(|field| Column { - name: field.name, - table_id: field.table_id, - type_info: PgTypeInfo::new(field.type_id), - }) - .collect::>() + result_columns: self + .map_result_columns(result_fields, type_names) + .await? .into_boxed_slice(), }) } + async fn get_type_names( + &mut self, + ids: impl IntoIterator, + ) -> crate::Result> { + let type_ids: HashSet = ids.into_iter().map(|id| id.0).collect::>(); + + if type_ids.is_empty() { + return Ok(HashMap::new()); + } + + // uppercase type names are easier to visually identify + let mut query = "select types.type_id, UPPER(pg_type.typname) from (VALUES ".to_string(); + let mut args = PgArguments::default(); + let mut pushed = false; + + // TODO: dedup this with the one below, ideally as an API we can export + for (i, (&type_id, bind)) in type_ids.iter().zip((1..).step_by(2)).enumerate() { + if pushed { + query += ", "; + } + + pushed = true; + let _ = write!(query, "(${}, ${})", bind, bind + 1); + + // not used in the output but ensures are values are sorted correctly + args.add(i as i32); + args.add(type_id as i32); + } + + query += ") as types(idx, type_id) \ + inner join pg_catalog.pg_type on pg_type.oid = type_id \ + order by types.idx"; + + crate::query::query(&query) + .bind_all(args) + .map(|row: PgRow| -> crate::Result<(u32, SharedStr)> { + Ok(( + row.get::(0)? as u32, + row.get::(1)?.into(), + )) + }) + .fetch(self) + .try_collect() + .await + } + + async fn map_result_columns( + &mut self, + fields: Box<[Field]>, + type_names: HashMap, + ) -> crate::Result>> { + if fields.is_empty() { + return Ok(vec![]); + } + + let mut query = "select col.idx, pg_attribute.attnotnull from (VALUES ".to_string(); + let mut pushed = false; + let mut args = PgArguments::default(); + + for (i, (field, bind)) in fields.iter().zip((1..).step_by(3)).enumerate() { + if pushed { + query += ", "; + } + + pushed = true; + let _ = write!( + query, + "(${}::int4, ${}::int4, ${}::int2)", + bind, + bind + 1, + bind + 2 + ); + + args.add(i as i32); + args.add(field.table_id.map(|id| id as i32)); + args.add(field.column_id); + } + + query += ") as col(idx, table_id, col_idx) \ + left join pg_catalog.pg_attribute on table_id is not null and attrelid = table_id and attnum = col_idx \ + order by col.idx;"; + + log::trace!("describe pg_attribute query: {:#?}", query); + + crate::query::query(&query) + .bind_all(args) + .map(|row: PgRow| { + let idx = row.get::(0)?; + let non_null = row.get::, _>(1)?; + + Ok((idx, non_null)) + }) + .fetch(self) + .zip(stream::iter(fields.into_vec().into_iter().enumerate())) + .map(|(row, (fidx, field))| -> crate::Result> { + let (idx, non_null) = row?; + + if idx != fidx as i32 { + return Err( + protocol_err!("missing field from query, field: {:?}", field).into(), + ); + } + + Ok(Column { + name: field.name, + table_id: field.table_id, + type_info: PgTypeInfo::new(field.type_id, &type_names[&field.type_id.0]), + non_null, + }) + }) + .try_collect() + .await + } + // Poll messages from Postgres, counting the rows affected, until we finish the query // This must be called directly after a call to [PgConnection::execute] async fn affected_rows(&mut self) -> crate::Result { diff --git a/sqlx-core/src/postgres/protocol/statement.rs b/sqlx-core/src/postgres/protocol/statement.rs index 821b5e4a4..be0cb25e9 100644 --- a/sqlx-core/src/postgres/protocol/statement.rs +++ b/sqlx-core/src/postgres/protocol/statement.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use crate::io::BufMut; use crate::postgres::protocol::Encode; @@ -7,10 +9,7 @@ pub struct StatementId(pub u32); impl Encode for StatementId { fn encode(&self, buf: &mut Vec) { if self.0 != 0 { - buf.put_str("__sqlx_statement_"); - - // TODO: Use [itoa] - buf.put_str_nul(&self.0.to_string()); + let _ = write!(buf, "__sqlx_statement_{}\0", self.0); } else { buf.put_str_nul(""); } diff --git a/sqlx-core/src/postgres/protocol/type_id.rs b/sqlx-core/src/postgres/protocol/type_id.rs index 94e6375bb..c083dde07 100644 --- a/sqlx-core/src/postgres/protocol/type_id.rs +++ b/sqlx-core/src/postgres/protocol/type_id.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TypeId(pub(crate) u32); #[allow(dead_code)] diff --git a/sqlx-core/src/postgres/types/bool.rs b/sqlx-core/src/postgres/types/bool.rs index cd92ea831..7f850dc1b 100644 --- a/sqlx-core/src/postgres/types/bool.rs +++ b/sqlx-core/src/postgres/types/bool.rs @@ -10,13 +10,13 @@ use crate::types::Type; impl Type for bool { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::BOOL) + PgTypeInfo::new(TypeId::BOOL, "BOOL") } } impl Type for [bool] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_BOOL) + PgTypeInfo::new(TypeId::ARRAY_BOOL, "BOOL[]") } } diff --git a/sqlx-core/src/postgres/types/bytes.rs b/sqlx-core/src/postgres/types/bytes.rs index e85b8c26c..5e0a81a6d 100644 --- a/sqlx-core/src/postgres/types/bytes.rs +++ b/sqlx-core/src/postgres/types/bytes.rs @@ -9,13 +9,13 @@ use crate::types::Type; impl Type for [u8] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::BYTEA) + PgTypeInfo::new(TypeId::BYTEA, "BYTEA") } } impl Type for [&'_ [u8]] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_BYTEA) + PgTypeInfo::new(TypeId::ARRAY_BYTEA, "BYTEA[]") } } diff --git a/sqlx-core/src/postgres/types/chrono.rs b/sqlx-core/src/postgres/types/chrono.rs index dd3663c03..f058f9f78 100644 --- a/sqlx-core/src/postgres/types/chrono.rs +++ b/sqlx-core/src/postgres/types/chrono.rs @@ -15,19 +15,19 @@ use crate::Error; impl Type for NaiveTime { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::TIME) + PgTypeInfo::new(TypeId::TIME, "TIME") } } impl Type for NaiveDate { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::DATE) + PgTypeInfo::new(TypeId::DATE, "DATE") } } impl Type for NaiveDateTime { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::TIMESTAMP) + PgTypeInfo::new(TypeId::TIMESTAMP, "TIMESTAMP") } } @@ -36,25 +36,25 @@ where Tz: TimeZone, { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::TIMESTAMPTZ) + PgTypeInfo::new(TypeId::TIMESTAMPTZ, "TIMESTAMPTZ") } } impl Type for [NaiveTime] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_TIME) + PgTypeInfo::new(TypeId::ARRAY_TIME, "TIME[]") } } impl Type for [NaiveDate] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_DATE) + PgTypeInfo::new(TypeId::ARRAY_DATE, "DATE[]") } } impl Type for [NaiveDateTime] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_TIMESTAMP) + PgTypeInfo::new(TypeId::ARRAY_TIMESTAMP, "TIMESTAMP[]") } } @@ -63,7 +63,7 @@ where Tz: TimeZone, { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_TIMESTAMPTZ) + PgTypeInfo::new(TypeId::ARRAY_TIMESTAMPTZ, "TIMESTAMP[]") } } diff --git a/sqlx-core/src/postgres/types/float.rs b/sqlx-core/src/postgres/types/float.rs index 3603ae66c..b434525dc 100644 --- a/sqlx-core/src/postgres/types/float.rs +++ b/sqlx-core/src/postgres/types/float.rs @@ -13,13 +13,13 @@ use crate::types::Type; impl Type for f32 { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::FLOAT4) + PgTypeInfo::new(TypeId::FLOAT4, "FLOAT4") } } impl Type for [f32] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_FLOAT4) + PgTypeInfo::new(TypeId::ARRAY_FLOAT4, "FLOAT4[]") } } @@ -44,13 +44,13 @@ impl<'de> Decode<'de, Postgres> for f32 { impl Type for f64 { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::FLOAT8) + PgTypeInfo::new(TypeId::FLOAT8, "FLOAT8") } } impl Type for [f64] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_FLOAT8) + PgTypeInfo::new(TypeId::ARRAY_FLOAT8, "FLOAT8[]") } } diff --git a/sqlx-core/src/postgres/types/int.rs b/sqlx-core/src/postgres/types/int.rs index 8f0eace77..f1d1d3bae 100644 --- a/sqlx-core/src/postgres/types/int.rs +++ b/sqlx-core/src/postgres/types/int.rs @@ -13,13 +13,13 @@ use crate::Error; impl Type for i16 { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::INT2) + PgTypeInfo::new(TypeId::INT2, "INT2") } } impl Type for [i16] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_INT2) + PgTypeInfo::new(TypeId::ARRAY_INT2, "INT2[]") } } @@ -40,13 +40,13 @@ impl<'de> Decode<'de, Postgres> for i16 { impl Type for i32 { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::INT4) + PgTypeInfo::new(TypeId::INT4, "INT4") } } impl Type for [i32] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_INT4) + PgTypeInfo::new(TypeId::ARRAY_INT4, "INT4[]") } } @@ -67,13 +67,13 @@ impl<'de> Decode<'de, Postgres> for i32 { impl Type for i64 { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::INT8) + PgTypeInfo::new(TypeId::INT8, "INT8") } } impl Type for [i64] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_INT8) + PgTypeInfo::new(TypeId::ARRAY_INT8, "INT8[]") } } diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs index e5fbd331b..f017ac7f5 100644 --- a/sqlx-core/src/postgres/types/mod.rs +++ b/sqlx-core/src/postgres/types/mod.rs @@ -1,4 +1,6 @@ use std::fmt::{self, Debug, Display}; +use std::ops::Deref; +use std::sync::Arc; use crate::decode::Decode; use crate::postgres::protocol::TypeId; @@ -20,11 +22,15 @@ mod uuid; #[derive(Debug, Clone)] pub struct PgTypeInfo { pub(crate) id: TypeId, + pub(crate) name: Option, } impl PgTypeInfo { - pub(crate) fn new(id: TypeId) -> Self { - Self { id } + pub(crate) fn new(id: TypeId, name: impl Into) -> Self { + Self { + id, + name: Some(name.into()), + } } /// Create a `PgTypeInfo` from a type's object identifier. @@ -32,14 +38,34 @@ impl PgTypeInfo { /// The object identifier of a type can be queried with /// `SELECT oid FROM pg_type WHERE typname = ;` pub fn with_oid(oid: u32) -> Self { - Self { id: TypeId(oid) } + Self { + id: TypeId(oid), + name: None, + } + } + + #[doc(hidden)] + pub fn type_name(&self) -> &str { + self.name.as_deref().unwrap_or("") + } + + #[doc(hidden)] + pub fn type_feature_gate(&self) -> Option<&'static str> { + match self.id { + TypeId::DATE | TypeId::TIME | TypeId::TIMESTAMP | TypeId::TIMESTAMPTZ => Some("chrono"), + TypeId::UUID => Some("uuid"), + _ => None, + } } } impl Display for PgTypeInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: Should we attempt to render the type *name* here? - write!(f, "{}", self.id.0) + if let Some(ref name) = self.name { + write!(f, "{}", *name) + } else { + write!(f, "OID {}", self.id.0) + } } } @@ -60,3 +86,46 @@ where .transpose() } } + +/// Copy of `Cow` but for strings; clones guaranteed to be cheap. +#[derive(Clone, Debug)] +pub(crate) enum SharedStr { + Static(&'static str), + Arc(Arc), +} + +impl Deref for SharedStr { + type Target = str; + + fn deref(&self) -> &str { + match self { + SharedStr::Static(s) => s, + SharedStr::Arc(s) => s, + } + } +} + +impl<'a> From<&'a SharedStr> for SharedStr { + fn from(s: &'a SharedStr) -> Self { + s.clone() + } +} + +impl From<&'static str> for SharedStr { + fn from(s: &'static str) -> Self { + SharedStr::Static(s) + } +} + +impl From for SharedStr { + #[inline] + fn from(s: String) -> Self { + SharedStr::Arc(s.into()) + } +} + +impl fmt::Display for SharedStr { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.pad(self) + } +} diff --git a/sqlx-core/src/postgres/types/str.rs b/sqlx-core/src/postgres/types/str.rs index bfd8946e2..2cd148768 100644 --- a/sqlx-core/src/postgres/types/str.rs +++ b/sqlx-core/src/postgres/types/str.rs @@ -12,13 +12,13 @@ use crate::Error; impl Type for str { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::TEXT) + PgTypeInfo::new(TypeId::TEXT, "TEXT") } } impl Type for [&'_ str] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_TEXT) + PgTypeInfo::new(TypeId::ARRAY_TEXT, "TEXT[]") } } diff --git a/sqlx-core/src/postgres/types/uuid.rs b/sqlx-core/src/postgres/types/uuid.rs index 1e6ea2644..b317968e3 100644 --- a/sqlx-core/src/postgres/types/uuid.rs +++ b/sqlx-core/src/postgres/types/uuid.rs @@ -13,13 +13,13 @@ use crate::types::Type; impl Type for Uuid { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::UUID) + PgTypeInfo::new(TypeId::UUID, "UUID") } } impl Type for [Uuid] { fn type_info() -> PgTypeInfo { - PgTypeInfo::new(TypeId::ARRAY_UUID) + PgTypeInfo::new(TypeId::ARRAY_UUID, "UUID[]") } } diff --git a/sqlx-macros/src/database/mod.rs b/sqlx-macros/src/database/mod.rs index 4e1731b2b..4a2eb77e6 100644 --- a/sqlx-macros/src/database/mod.rs +++ b/sqlx-macros/src/database/mod.rs @@ -24,10 +24,19 @@ pub trait DatabaseExt: Database { fn param_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>; fn return_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>; + + fn get_feature_gate(info: &Self::TypeInfo) -> Option<&'static str>; } macro_rules! impl_database_ext { - ($database:path { $($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)? }, ParamChecking::$param_checking:ident, row = $row:path) => { + ( + $database:path { + $($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)? + }, + ParamChecking::$param_checking:ident, + feature-types: $name:ident => $get_gate:expr, + row = $row:path + ) => { impl $crate::database::DatabaseExt for $database { const DATABASE_PATH: &'static str = stringify!($database); const ROW_PATH: &'static str = stringify!($row); @@ -53,6 +62,10 @@ macro_rules! impl_database_ext { _ => None } } + + fn get_feature_gate($name: &Self::TypeInfo) -> Option<&'static str> { + $get_gate + } } } } diff --git a/sqlx-macros/src/database/mysql.rs b/sqlx-macros/src/database/mysql.rs index a86b7b1f6..6099959d9 100644 --- a/sqlx-macros/src/database/mysql.rs +++ b/sqlx-macros/src/database/mysql.rs @@ -30,5 +30,6 @@ impl_database_ext! { sqlx::types::chrono::DateTime, }, ParamChecking::Weak, + feature-types: info => info.type_feature_gate(), row = sqlx::mysql::MySqlRow } diff --git a/sqlx-macros/src/database/postgres.rs b/sqlx-macros/src/database/postgres.rs index 6051ef785..c84da77cd 100644 --- a/sqlx-macros/src/database/postgres.rs +++ b/sqlx-macros/src/database/postgres.rs @@ -27,5 +27,6 @@ impl_database_ext! { sqlx::types::chrono::DateTime | sqlx::types::chrono::DateTime<_>, }, ParamChecking::Strong, + feature-types: info => info.type_feature_gate(), row = sqlx::postgres::PgRow } diff --git a/sqlx-macros/src/query_macros/args.rs b/sqlx-macros/src/query_macros/args.rs index f9767103b..438b777b6 100644 --- a/sqlx-macros/src/query_macros/args.rs +++ b/sqlx-macros/src/query_macros/args.rs @@ -22,7 +22,8 @@ pub fn quote_args( .param_types .iter() .zip(&*input.arg_exprs) - .map(|(type_, expr)| { + .enumerate() + .map(|(i, (type_, expr))| { get_type_override(expr) .or_else(|| { Some( @@ -31,7 +32,19 @@ pub fn quote_args( .unwrap(), ) }) - .ok_or_else(|| format!("unknown type param ID: {}", type_).into()) + .ok_or_else(|| { + if let Some(feature_gate) = ::get_feature_gate(&type_) { + format!( + "optional feature `{}` required for type {} of param #{}", + feature_gate, + type_, + i + 1, + ) + .into() + } else { + format!("unsupported type {} for param #{}", type_, i + 1).into() + } + }) }) .collect::>>()?; diff --git a/sqlx-macros/src/query_macros/output.rs b/sqlx-macros/src/query_macros/output.rs index 55996aab4..ec9f9c373 100644 --- a/sqlx-macros/src/query_macros/output.rs +++ b/sqlx-macros/src/query_macros/output.rs @@ -6,11 +6,31 @@ use sqlx::describe::Describe; use crate::database::DatabaseExt; +use std::fmt::{self, Display, Formatter}; + pub struct RustColumn { pub(super) ident: Ident, pub(super) type_: TokenStream, } +struct DisplayColumn<'a> { + // zero-based index, converted to 1-based number + idx: usize, + name: Option<&'a str>, +} + +impl Display for DisplayColumn<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let num = self.idx + 1; + + if let Some(name) = self.name { + write!(f, "column #{} ({:?})", num, name) + } else { + write!(f, "column #{}", num) + } + } +} + pub fn columns_to_rust(describe: &Describe) -> crate::Result> { describe .result_columns @@ -25,7 +45,30 @@ pub fn columns_to_rust(describe: &Describe) -> crate::Resul let ident = parse_ident(name)?; let type_ = ::return_type_for_id(&column.type_info) - .ok_or_else(|| format!("unknown type: {}", &column.type_info))? + .ok_or_else(|| { + if let Some(feature_gate) = + ::get_feature_gate(&column.type_info) + { + format!( + "optional feature `{feat}` required for type {ty} of {col}", + ty = &column.type_info, + feat = feature_gate, + col = DisplayColumn { + idx: i, + name: column.name.as_deref() + } + ) + } else { + format!( + "unsupported type {ty} of {col}", + ty = column.type_info, + col = DisplayColumn { + idx: i, + name: column.name.as_deref() + } + ) + } + })? .parse::() .unwrap(); diff --git a/tests/mysql.rs b/tests/mysql.rs index b67ddb9dd..5eb53beca 100644 --- a/tests/mysql.rs +++ b/tests/mysql.rs @@ -65,6 +65,49 @@ async fn it_selects_null() -> anyhow::Result<()> { Ok(()) } +#[cfg_attr(feature = "runtime-async-std", async_std::test)] +#[cfg_attr(feature = "runtime-tokio", tokio::test)] +async fn test_describe() -> anyhow::Result<()> { + use sqlx::describe::Nullability::*; + + let mut conn = connect().await?; + + let _ = conn + .send( + r#" + CREATE TEMPORARY TABLE describe_test ( + id int primary key auto_increment, + name text not null, + hash blob + ) + "#, + ) + .await?; + + let describe = conn + .describe("select nt.*, false from describe_test nt") + .await?; + + assert_eq!(describe.result_columns[0].nullability, NonNull); + assert_eq!(describe.result_columns[0].type_info.type_name(), "INT"); + assert_eq!(describe.result_columns[1].nullability, NonNull); + assert_eq!(describe.result_columns[1].type_info.type_name(), "TEXT"); + assert_eq!(describe.result_columns[2].nullability, Nullable); + assert_eq!(describe.result_columns[2].type_info.type_name(), "TEXT"); + assert_eq!(describe.result_columns[3].nullability, NonNull); + + let bool_ty_name = describe.result_columns[3].type_info.type_name(); + + // MySQL 5.7, 8 and MariaDB 10.1 return BIG_INT, MariaDB 10.4 returns INT (optimization?) + assert!( + ["BIG_INT", "INT"].contains(&bool_ty_name), + "type name returned: {}", + bool_ty_name + ); + + Ok(()) +} + #[cfg_attr(feature = "runtime-async-std", async_std::test)] #[cfg_attr(feature = "runtime-tokio", tokio::test)] async fn pool_immediately_fails_with_db_error() -> anyhow::Result<()> { diff --git a/tests/postgres.rs b/tests/postgres.rs index 63bc0d725..84d667862 100644 --- a/tests/postgres.rs +++ b/tests/postgres.rs @@ -141,6 +141,39 @@ async fn pool_smoke_test() -> anyhow::Result<()> { Ok(()) } +#[cfg_attr(feature = "runtime-async-std", async_std::test)] +#[cfg_attr(feature = "runtime-tokio", tokio::test)] +async fn test_describe() -> anyhow::Result<()> { + let mut conn = connect().await?; + + let _ = conn + .execute( + r#" + CREATE TEMP TABLE describe_test ( + id SERIAL primary key, + name text not null, + hash bytea + ) + "#, + ) + .await?; + + let describe = conn + .describe("select nt.*, false from describe_test nt") + .await?; + + assert_eq!(describe.result_columns[0].non_null, Some(true)); + assert_eq!(describe.result_columns[0].type_info.type_name(), "INT4"); + assert_eq!(describe.result_columns[1].non_null, Some(true)); + assert_eq!(describe.result_columns[1].type_info.type_name(), "TEXT"); + assert_eq!(describe.result_columns[2].non_null, Some(false)); + assert_eq!(describe.result_columns[2].type_info.type_name(), "BYTEA"); + assert_eq!(describe.result_columns[3].non_null, None); + assert_eq!(describe.result_columns[3].type_info.type_name(), "BOOL"); + + Ok(()) +} + async fn connect() -> anyhow::Result { let _ = dotenv::dotenv(); let _ = env_logger::try_init(); diff --git a/tests/ui-tests.rs b/tests/ui-tests.rs index a525b7539..a7b327ff1 100644 --- a/tests/ui-tests.rs +++ b/tests/ui-tests.rs @@ -4,10 +4,24 @@ fn ui_tests() { if cfg!(feature = "postgres") { t.compile_fail("tests/ui/postgres/*.rs"); + + // UI tests for column types that require gated features + if cfg!(not(feature = "chrono")) { + t.compile_fail("tests/ui/postgres/gated/chrono.rs"); + } + + if cfg!(not(feature = "uuid")) { + t.compile_fail("tests/ui/postgres/gated/uuid.rs"); + } } if cfg!(feature = "mysql") { t.compile_fail("tests/ui/mysql/*.rs"); + + // UI tests for column types that require gated features + if cfg!(not(feature = "chrono")) { + t.compile_fail("tests/ui/mysql/gated/chrono.rs"); + } } t.compile_fail("tests/ui/*.rs"); diff --git a/tests/ui/mysql/gated/chrono.rs b/tests/ui/mysql/gated/chrono.rs new file mode 100644 index 000000000..76b590b92 --- /dev/null +++ b/tests/ui/mysql/gated/chrono.rs @@ -0,0 +1,7 @@ +fn main() { + let _ = sqlx::query!("select CONVERT(now(), DATE) date"); + + let _ = sqlx::query!("select CONVERT(now(), TIME) time"); + + let _ = sqlx::query!("select CONVERT(now(), DATETIME) datetime"); +} diff --git a/tests/ui/mysql/gated/chrono.stderr b/tests/ui/mysql/gated/chrono.stderr new file mode 100644 index 000000000..b2bcd1c71 --- /dev/null +++ b/tests/ui/mysql/gated/chrono.stderr @@ -0,0 +1,23 @@ +error: optional feature `chrono` required for type DATE of column #1 ("date") + --> $DIR/chrono.rs:2:13 + | +2 | let _ = sqlx::query!("select CONVERT(now(), DATE) date"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) + +error: optional feature `chrono` required for type TIME of column #1 ("time") + --> $DIR/chrono.rs:4:13 + | +4 | let _ = sqlx::query!("select CONVERT(now(), TIME) time"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) + +error: optional feature `chrono` required for type DATETIME of column #1 ("datetime") + --> $DIR/chrono.rs:6:13 + | +6 | let _ = sqlx::query!("select CONVERT(now(), DATETIME) datetime"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) diff --git a/tests/ui/postgres/gated/chrono.rs b/tests/ui/postgres/gated/chrono.rs new file mode 100644 index 000000000..cc501d1d1 --- /dev/null +++ b/tests/ui/postgres/gated/chrono.rs @@ -0,0 +1,17 @@ +fn main() { + let _ = sqlx::query!("select now()::date"); + + let _ = sqlx::query!("select now()::time"); + + let _ = sqlx::query!("select now()::timestamp"); + + let _ = sqlx::query!("select now()::timestamptz"); + + let _ = sqlx::query!("select $1::date", ()); + + let _ = sqlx::query!("select $1::time", ()); + + let _ = sqlx::query!("select $1::timestamp", ()); + + let _ = sqlx::query!("select $1::timestamptz", ()); +} diff --git a/tests/ui/postgres/gated/chrono.stderr b/tests/ui/postgres/gated/chrono.stderr new file mode 100644 index 000000000..c6f42af44 --- /dev/null +++ b/tests/ui/postgres/gated/chrono.stderr @@ -0,0 +1,63 @@ +error: optional feature `chrono` required for type DATE of column #1 ("now") + --> $DIR/chrono.rs:2:13 + | +2 | let _ = sqlx::query!("select now()::date"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type TIME of column #1 ("now") + --> $DIR/chrono.rs:4:13 + | +4 | let _ = sqlx::query!("select now()::time"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type TIMESTAMP of column #1 ("now") + --> $DIR/chrono.rs:6:13 + | +6 | let _ = sqlx::query!("select now()::timestamp"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type TIMESTAMPTZ of column #1 ("now") + --> $DIR/chrono.rs:8:13 + | +8 | let _ = sqlx::query!("select now()::timestamptz"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type DATE of param #1 + --> $DIR/chrono.rs:10:13 + | +10 | let _ = sqlx::query!("select $1::date", ()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type TIME of param #1 + --> $DIR/chrono.rs:12:13 + | +12 | let _ = sqlx::query!("select $1::time", ()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type TIMESTAMP of param #1 + --> $DIR/chrono.rs:14:13 + | +14 | let _ = sqlx::query!("select $1::timestamp", ()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `chrono` required for type TIMESTAMPTZ of param #1 + --> $DIR/chrono.rs:16:13 + | +16 | let _ = sqlx::query!("select $1::timestamptz", ()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/postgres/gated/uuid.rs b/tests/ui/postgres/gated/uuid.rs new file mode 100644 index 000000000..d78970df9 --- /dev/null +++ b/tests/ui/postgres/gated/uuid.rs @@ -0,0 +1,4 @@ +fn main() { + let _ = sqlx::query!("select 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid"); + let _ = sqlx::query!("select $1::uuid", ()); +} diff --git a/tests/ui/postgres/gated/uuid.stderr b/tests/ui/postgres/gated/uuid.stderr new file mode 100644 index 000000000..4ba22c05f --- /dev/null +++ b/tests/ui/postgres/gated/uuid.stderr @@ -0,0 +1,15 @@ +error: optional feature `uuid` required for type UUID of column #1 ("uuid") + --> $DIR/uuid.rs:2:13 + | +2 | let _ = sqlx::query!("select 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: optional feature `uuid` required for type UUID of param #1 + --> $DIR/uuid.rs:3:13 + | +3 | let _ = sqlx::query!("select $1::uuid", ()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/postgres/unsupported-type.rs b/tests/ui/postgres/unsupported-type.rs new file mode 100644 index 000000000..f9be2025d --- /dev/null +++ b/tests/ui/postgres/unsupported-type.rs @@ -0,0 +1,5 @@ +fn main() { + // we're probably not going to get around to the geometric types anytime soon + let _ = sqlx::query!("select null::circle"); + let _ = sqlx::query!("select $1::circle", panic!()); +} diff --git a/tests/ui/postgres/unsupported-type.stderr b/tests/ui/postgres/unsupported-type.stderr new file mode 100644 index 000000000..c375f816b --- /dev/null +++ b/tests/ui/postgres/unsupported-type.stderr @@ -0,0 +1,15 @@ +error: unsupported type CIRCLE of column #1 ("circle") + --> $DIR/unsupported-type.rs:3:13 + | +3 | let _ = sqlx::query!("select null::circle"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unsupported type CIRCLE for param #1 + --> $DIR/unsupported-type.rs:4:13 + | +4 | let _ = sqlx::query!("select $1::circle", panic!()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)