diff --git a/sqlx-core/src/describe.rs b/sqlx-core/src/describe.rs index 04d5eac12..9b7aa7426 100644 --- a/sqlx-core/src/describe.rs +++ b/sqlx-core/src/describe.rs @@ -11,7 +11,7 @@ where DB: Database + ?Sized, { /// The expected types for the parameters of the query. - pub param_types: Box<[DB::TypeInfo]>, + pub param_types: Box<[Option]>, /// The type and table information, if any for the results of the query. pub result_columns: Box<[Column]>, @@ -39,7 +39,7 @@ where { pub name: Option>, pub table_id: Option, - pub type_info: DB::TypeInfo, + pub type_info: Option, /// Whether or not the column cannot be `NULL` (or if that is even knowable). pub non_null: Option, } diff --git a/sqlx-core/src/mysql/types/mod.rs b/sqlx-core/src/mysql/types/mod.rs index 6cf9ec205..187411e77 100644 --- a/sqlx-core/src/mysql/types/mod.rs +++ b/sqlx-core/src/mysql/types/mod.rs @@ -43,13 +43,17 @@ impl MySqlTypeInfo { } } - pub(crate) fn from_column_def(def: &ColumnDefinition) -> Self { - Self { + pub(crate) fn from_column_def(def: &ColumnDefinition) -> Option { + if def.type_id == TypeId::NULL { + return None; + } + + Some(Self { id: def.type_id, is_unsigned: def.flags.contains(FieldFlags::UNSIGNED), is_binary: def.flags.contains(FieldFlags::BINARY), char_set: def.char_set, - } + }) } #[doc(hidden)] @@ -106,10 +110,6 @@ 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/executor.rs b/sqlx-core/src/postgres/executor.rs index f17965ef7..926eaa208 100644 --- a/sqlx-core/src/postgres/executor.rs +++ b/sqlx-core/src/postgres/executor.rs @@ -199,7 +199,7 @@ impl PgConnection { param_types: params .ids .iter() - .map(|id| PgTypeInfo::new(*id, &type_names[&id.0])) + .map(|id| Some(PgTypeInfo::new(*id, &type_names[&id.0]))) .collect::>() .into_boxed_slice(), result_columns: self @@ -315,7 +315,10 @@ impl PgConnection { Ok(Column { name: field.name, table_id: field.table_id, - type_info: PgTypeInfo::new(field.type_id, &type_names[&field.type_id.0]), + type_info: Some(PgTypeInfo::new( + field.type_id, + &type_names[&field.type_id.0], + )), non_null, }) }) diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs index 5491e85c8..ab202c400 100644 --- a/sqlx-core/src/postgres/types/mod.rs +++ b/sqlx-core/src/postgres/types/mod.rs @@ -81,11 +81,6 @@ 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/executor.rs b/sqlx-core/src/sqlite/executor.rs index 6d58752a5..a145b4d74 100644 --- a/sqlx-core/src/sqlite/executor.rs +++ b/sqlx-core/src/sqlite/executor.rs @@ -138,14 +138,7 @@ impl Executor for SqliteConnection { // First let's attempt to describe what we can about parameter types // Which happens to just be the count, heh let num_params = statement.params(); - let params = vec![ - SqliteTypeInfo { - r#type: SqliteType::Null, - affinity: None, - }; - num_params - ] - .into_boxed_slice(); + let params = vec![None; num_params].into_boxed_slice(); // Next, collect (return) column types and names let num_columns = statement.column_count(); @@ -155,15 +148,15 @@ impl Executor for SqliteConnection { let decl = statement.column_decltype(i); let r#type = match decl { - None => SqliteType::Null, + None => None, Some(decl) => match &*decl.to_ascii_lowercase() { - "bool" | "boolean" => SqliteType::Boolean, - "clob" | "text" => SqliteType::Text, - "blob" => SqliteType::Blob, - "real" | "double" | "double precision" | "float" => SqliteType::Float, - decl @ _ if decl.contains("int") => SqliteType::Integer, - decl @ _ if decl.contains("char") => SqliteType::Text, - _ => SqliteType::Null, + "bool" | "boolean" => Some(SqliteType::Boolean), + "clob" | "text" => Some(SqliteType::Text), + "blob" => Some(SqliteType::Blob), + "real" | "double" | "double precision" | "float" => Some(SqliteType::Float), + decl @ _ if decl.contains("int") => Some(SqliteType::Integer), + decl @ _ if decl.contains("char") => Some(SqliteType::Text), + _ => None, }, }; @@ -171,10 +164,10 @@ impl Executor for SqliteConnection { name: Some(name.into()), non_null: None, table_id: None, - type_info: SqliteTypeInfo { + type_info: r#type.map(|r#type| SqliteTypeInfo { r#type, affinity: None, - }, + }), }) } diff --git a/sqlx-core/src/sqlite/types/mod.rs b/sqlx-core/src/sqlite/types/mod.rs index 14f887bb0..1c113683c 100644 --- a/sqlx-core/src/sqlite/types/mod.rs +++ b/sqlx-core/src/sqlite/types/mod.rs @@ -66,10 +66,6 @@ impl TypeInfo for SqliteTypeInfo { fn compatible(&self, other: &Self) -> bool { self.r#type == other.r#type || 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 4df0dbdff..1425acd2f 100644 --- a/sqlx-core/src/types.rs +++ b/sqlx-core/src/types.rs @@ -18,14 +18,6 @@ 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/args.rs b/sqlx-macros/src/query_macros/args.rs index ce08efc0b..ac2218aeb 100644 --- a/sqlx-macros/src/query_macros/args.rs +++ b/sqlx-macros/src/query_macros/args.rs @@ -30,11 +30,14 @@ pub fn quote_args( .iter() .zip(input.arg_names.iter().zip(&input.arg_exprs)) .enumerate() - .map(|(i, (param_ty, (name, expr)))| -> crate::Result<_>{ + .map(|(i, (param_ty, (name, expr)))| -> crate::Result<_> { + // TODO: We could remove the ParamChecking flag and just filter to only test params that are non-null + let param_ty = param_ty.as_ref().unwrap(); + let param_ty = get_type_override(expr) .or_else(|| { Some( - DB::param_type_for_id(param_ty)? + DB::param_type_for_id(¶m_ty)? .parse::() .unwrap(), ) diff --git a/sqlx-macros/src/query_macros/output.rs b/sqlx-macros/src/query_macros/output.rs index 4bc8eb8fe..62da0ec79 100644 --- a/sqlx-macros/src/query_macros/output.rs +++ b/sqlx-macros/src/query_macros/output.rs @@ -45,44 +45,51 @@ 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(|| { - 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 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}", - ty = column.type_info, - col = DisplayColumn { - idx: i, - name: column.name.as_deref() - } - ) + let type_ = if let Some(type_info) = &column.type_info { + ::return_type_for_id(&type_info) + .ok_or_else(|| { + if let Some(feature_gate) = + ::get_feature_gate(&type_info) + { + format!( + "optional feature `{feat}` required for type {ty} of {col}", + ty = &type_info, + feat = feature_gate, + col = DisplayColumn { + idx: i, + name: column.name.as_deref() + } + ) + } else { + format!( + "unsupported type {ty} of {col}", + ty = type_info, + col = DisplayColumn { + idx: i, + name: column.name.as_deref() + } + ) + } + })? + .parse::() + .unwrap() + } else { + 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() } - })? + ) .parse::() - .unwrap(); + .unwrap() + }; - Ok(RustColumn { ident, type_ }) + Ok(RustColumn { + ident, + type_: type_, + }) }) .collect::>>() } diff --git a/tests/postgres.rs b/tests/postgres.rs index 9f11d87fd..30dbfacc3 100644 --- a/tests/postgres.rs +++ b/tests/postgres.rs @@ -230,13 +230,41 @@ async fn test_describe() -> anyhow::Result<()> { .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[0] + .type_info + .as_ref() + .unwrap() + .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[1] + .type_info + .as_ref() + .unwrap() + .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[2] + .type_info + .as_ref() + .unwrap() + .type_name(), + "BYTEA" + ); assert_eq!(describe.result_columns[3].non_null, None); - assert_eq!(describe.result_columns[3].type_info.type_name(), "BOOL"); + assert_eq!( + describe.result_columns[3] + .type_info + .as_ref() + .unwrap() + .type_name(), + "BOOL" + ); Ok(()) } diff --git a/tests/sqlite-macros.rs b/tests/sqlite-macros.rs index 13642c614..0c73d59a9 100644 --- a/tests/sqlite-macros.rs +++ b/tests/sqlite-macros.rs @@ -21,9 +21,12 @@ async fn macro_select() -> anyhow::Result<()> { 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?; + 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); diff --git a/tests/sqlite.rs b/tests/sqlite.rs index 48fd696e6..688abed9b 100644 --- a/tests/sqlite.rs +++ b/tests/sqlite.rs @@ -129,17 +129,73 @@ CREATE TEMPORARY TABLE describe_test ( .describe("select nt.*, false from describe_test nt") .await?; - assert_eq!(describe.result_columns[0].type_info.to_string(), "INTEGER"); - assert_eq!(describe.result_columns[1].type_info.to_string(), "TEXT"); - assert_eq!(describe.result_columns[2].type_info.to_string(), "BLOB"); - assert_eq!(describe.result_columns[3].type_info.to_string(), "BOOLEAN"); - assert_eq!(describe.result_columns[4].type_info.to_string(), "DOUBLE"); - assert_eq!(describe.result_columns[5].type_info.to_string(), "TEXT"); - assert_eq!(describe.result_columns[6].type_info.to_string(), "DOUBLE"); - assert_eq!(describe.result_columns[7].type_info.to_string(), "INTEGER"); + assert_eq!( + describe.result_columns[0] + .type_info + .as_ref() + .unwrap() + .to_string(), + "INTEGER" + ); + assert_eq!( + describe.result_columns[1] + .type_info + .as_ref() + .unwrap() + .to_string(), + "TEXT" + ); + assert_eq!( + describe.result_columns[2] + .type_info + .as_ref() + .unwrap() + .to_string(), + "BLOB" + ); + assert_eq!( + describe.result_columns[3] + .type_info + .as_ref() + .unwrap() + .to_string(), + "BOOLEAN" + ); + assert_eq!( + describe.result_columns[4] + .type_info + .as_ref() + .unwrap() + .to_string(), + "DOUBLE" + ); + assert_eq!( + describe.result_columns[5] + .type_info + .as_ref() + .unwrap() + .to_string(), + "TEXT" + ); + assert_eq!( + describe.result_columns[6] + .type_info + .as_ref() + .unwrap() + .to_string(), + "DOUBLE" + ); + assert_eq!( + describe.result_columns[7] + .type_info + .as_ref() + .unwrap() + .to_string(), + "INTEGER" + ); // Expressions can not be described - assert_eq!(describe.result_columns[8].type_info.to_string(), "NULL"); + assert!(describe.result_columns[8].type_info.is_none()); Ok(()) }