From 13f6ef0ab060bf2b9c0b6eea4e130f1f48d05262 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Thu, 19 Sep 2024 22:54:48 -0700 Subject: [PATCH] feat: make macros aware of `macros.preferred-crates` --- sqlx-core/src/column.rs | 22 +- sqlx-core/src/config/common.rs | 4 +- sqlx-core/src/config/macros.rs | 39 ++- sqlx-core/src/config/migrate.rs | 4 +- sqlx-core/src/config/mod.rs | 23 +- sqlx-core/src/config/reference.toml | 11 +- sqlx-core/src/config/tests.rs | 13 +- sqlx-core/src/type_checking.rs | 298 ++++++++++++++++++++-- sqlx-macros-core/src/query/args.rs | 78 ++++-- sqlx-macros-core/src/query/mod.rs | 38 ++- sqlx-macros-core/src/query/output.rs | 143 +++++++---- sqlx-mysql/src/connection/executor.rs | 6 +- sqlx-mysql/src/protocol/text/column.rs | 2 +- sqlx-mysql/src/type_checking.rs | 62 +++-- sqlx-postgres/src/column.rs | 4 +- sqlx-postgres/src/connection/describe.rs | 104 ++++---- sqlx-postgres/src/connection/establish.rs | 4 +- sqlx-postgres/src/type_checking.rs | 230 ++++++++--------- sqlx-sqlite/src/column.rs | 2 +- sqlx-sqlite/src/connection/describe.rs | 2 +- sqlx-sqlite/src/statement/handle.rs | 33 +-- sqlx-sqlite/src/statement/virtual.rs | 1 + sqlx-sqlite/src/type_checking.rs | 45 ++-- src/lib.rs | 24 ++ 24 files changed, 814 insertions(+), 378 deletions(-) diff --git a/sqlx-core/src/column.rs b/sqlx-core/src/column.rs index 74833757..fddc048c 100644 --- a/sqlx-core/src/column.rs +++ b/sqlx-core/src/column.rs @@ -23,15 +23,17 @@ pub trait Column: 'static + Send + Sync + Debug { fn type_info(&self) -> &::TypeInfo; /// If this column comes from a table, return the table and original column name. - /// + /// /// Returns [`ColumnOrigin::Expression`] if the column is the result of an expression /// or else the source table could not be determined. - /// + /// /// Returns [`ColumnOrigin::Unknown`] if the database driver does not have that information, /// or has not overridden this method. - // This method returns an owned value instead of a reference, + // This method returns an owned value instead of a reference, // to give the implementor more flexibility. - fn origin(&self) -> ColumnOrigin { ColumnOrigin::Unknown } + fn origin(&self) -> ColumnOrigin { + ColumnOrigin::Unknown + } } /// A [`Column`] that originates from a table. @@ -44,20 +46,20 @@ pub struct TableColumn { pub name: Arc, } -/// The possible statuses for our knowledge of the origin of a [`Column`]. +/// The possible statuses for our knowledge of the origin of a [`Column`]. #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))] pub enum ColumnOrigin { - /// The column is known to originate from a table. - /// - /// Included is the table name and original column name. + /// The column is known to originate from a table. + /// + /// Included is the table name and original column name. Table(TableColumn), /// The column originates from an expression, or else its origin could not be determined. Expression, /// The database driver does not know the column origin at this time. - /// + /// /// This may happen if: - /// * The connection is in the middle of executing a query, + /// * The connection is in the middle of executing a query, /// and cannot query the catalog to fetch this information. /// * The connection does not have access to the database catalog. /// * The implementation of [`Column`] did not override [`Column::origin()`]. diff --git a/sqlx-core/src/config/common.rs b/sqlx-core/src/config/common.rs index c09ed80d..d2bf639e 100644 --- a/sqlx-core/src/config/common.rs +++ b/sqlx-core/src/config/common.rs @@ -44,6 +44,6 @@ pub struct Config { impl Config { pub fn database_url_var(&self) -> &str { - self.database_url_var.as_deref().unwrap_or("DATABASE_URL") + self.database_url_var.as_deref().unwrap_or("DATABASE_URL") } -} \ No newline at end of file +} diff --git a/sqlx-core/src/config/macros.rs b/sqlx-core/src/config/macros.rs index 9f4cf452..19e5f42f 100644 --- a/sqlx-core/src/config/macros.rs +++ b/sqlx-core/src/config/macros.rs @@ -3,13 +3,13 @@ use std::collections::BTreeMap; /// Configuration for the `query!()` family of macros. #[derive(Debug, Default)] #[cfg_attr( - feature = "sqlx-toml", - derive(serde::Deserialize), + feature = "sqlx-toml", + derive(serde::Deserialize), serde(default, rename_all = "kebab-case") )] pub struct Config { /// Specify which crates' types to use when types from multiple crates apply. - /// + /// /// See [`PreferredCrates`] for details. pub preferred_crates: PreferredCrates, @@ -18,6 +18,12 @@ pub struct Config { /// Default type mappings are defined by the database driver. /// Refer to the `sqlx::types` module for details. /// + /// ## Note: Case-Sensitive + /// Currently, the case of the type name MUST match the name SQLx knows it by. + /// Built-in types are spelled in all-uppercase to match SQL convention. + /// + /// However, user-created types in Postgres are all-lowercase unless quoted. + /// /// ## Note: Orthogonal to Nullability /// These overrides do not affect whether `query!()` decides to wrap a column in `Option<_>` /// or not. They only override the inner type used. @@ -63,7 +69,7 @@ pub struct Config { /// ```toml /// [macros.type-overrides] /// # Override a built-in type - /// 'uuid' = "crate::types::MyUuid" + /// 'UUID' = "crate::types::MyUuid" /// /// # Support an external or custom wrapper type (e.g. from the `isn` Postgres extension) /// # (NOTE: FOR DOCUMENTATION PURPOSES ONLY; THIS CRATE/TYPE DOES NOT EXIST AS OF WRITING) @@ -132,6 +138,8 @@ pub struct Config { /// ``` /// /// (See `Note` section above for details.) + // TODO: allow specifying different types for input vs output + // e.g. to accept `&[T]` on input but output `Vec` pub type_overrides: BTreeMap, /// Specify per-table and per-column overrides for mapping SQL types to Rust types. @@ -221,7 +229,7 @@ pub struct Config { #[cfg_attr( feature = "sqlx-toml", derive(serde::Deserialize), - serde(rename_all = "kebab-case") + serde(default, rename_all = "kebab-case") )] pub struct PreferredCrates { /// Specify the crate to use for mapping date/time types to Rust. @@ -360,6 +368,7 @@ pub type RustType = Box; impl Config { /// Get the override for a given type name (optionally schema-qualified). pub fn type_override(&self, type_name: &str) -> Option<&str> { + // TODO: make this case-insensitive self.type_overrides.get(type_name).map(|s| &**s) } @@ -378,6 +387,15 @@ impl DateTimeCrate { pub fn is_inferred(&self) -> bool { *self == Self::Inferred } + + #[inline(always)] + pub fn crate_name(&self) -> Option<&str> { + match self { + Self::Inferred => None, + Self::Chrono => Some("chrono"), + Self::Time => Some("time"), + } + } } impl NumericCrate { @@ -386,4 +404,13 @@ impl NumericCrate { pub fn is_inferred(&self) -> bool { *self == Self::Inferred } -} \ No newline at end of file + + #[inline(always)] + pub fn crate_name(&self) -> Option<&str> { + match self { + Self::Inferred => None, + Self::BigDecimal => Some("bigdecimal"), + Self::RustDecimal => Some("rust_decimal"), + } + } +} diff --git a/sqlx-core/src/config/migrate.rs b/sqlx-core/src/config/migrate.rs index d0e55b35..64529f9f 100644 --- a/sqlx-core/src/config/migrate.rs +++ b/sqlx-core/src/config/migrate.rs @@ -14,8 +14,8 @@ use std::collections::BTreeSet; /// Be sure you know what you are doing and that you read all relevant documentation _thoroughly_. #[derive(Debug, Default)] #[cfg_attr( - feature = "sqlx-toml", - derive(serde::Deserialize), + feature = "sqlx-toml", + derive(serde::Deserialize), serde(default, rename_all = "kebab-case") )] pub struct Config { diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs index 696752a5..b3afd9ea 100644 --- a/sqlx-core/src/config/mod.rs +++ b/sqlx-core/src/config/mod.rs @@ -86,9 +86,7 @@ pub enum ConfigError { /// No configuration file was found. Not necessarily fatal. #[error("config file {path:?} not found")] - NotFound { - path: PathBuf, - }, + NotFound { path: PathBuf }, /// An I/O error occurred while attempting to read the config file at `path`. /// @@ -103,7 +101,7 @@ pub enum ConfigError { /// An error in the TOML was encountered while parsing the config file at `path`. /// /// The error gives line numbers and context when printed with `Display`/`ToString`. - /// + /// /// Only returned if the `sqlx-toml` feature is enabled. #[error("error parsing config file {path:?}")] Parse { @@ -115,14 +113,12 @@ pub enum ConfigError { /// A `sqlx.toml` file was found or specified, but the `sqlx-toml` feature is not enabled. #[error("SQLx found config file at {path:?} but the `sqlx-toml` feature was not enabled")] - ParseDisabled { - path: PathBuf - }, + ParseDisabled { path: PathBuf }, } impl ConfigError { /// Create a [`ConfigError`] from a [`std::io::Error`]. - /// + /// /// Maps to either `NotFound` or `Io`. pub fn from_io(path: PathBuf, error: io::Error) -> Self { if error.kind() == io::ErrorKind::NotFound { @@ -131,7 +127,7 @@ impl ConfigError { Self::Io { path, error } } } - + /// If this error means the file was not found, return the path that was attempted. pub fn not_found_path(&self) -> Option<&Path> { if let Self::NotFound { path } = self { @@ -227,15 +223,18 @@ impl Config { // Motivation: https://github.com/toml-rs/toml/issues/761 tracing::debug!("read config TOML from {path:?}:\n{toml_s}"); - toml::from_str(&toml_s).map_err(|error| ConfigError::Parse { path, error: Box::new(error) }) + toml::from_str(&toml_s).map_err(|error| ConfigError::Parse { + path, + error: Box::new(error), + }) } - + #[cfg(not(feature = "sqlx-toml"))] fn read_from(path: PathBuf) -> Result { match path.try_exists() { Ok(true) => Err(ConfigError::ParseDisabled { path }), Ok(false) => Err(ConfigError::NotFound { path }), - Err(e) => Err(ConfigError::from_io(path, e)) + Err(e) => Err(ConfigError::from_io(path, e)), } } } diff --git a/sqlx-core/src/config/reference.toml b/sqlx-core/src/config/reference.toml index e042824c..77833fb5 100644 --- a/sqlx-core/src/config/reference.toml +++ b/sqlx-core/src/config/reference.toml @@ -30,7 +30,14 @@ date-time = "chrono" # in case new date/time crates are added in the future: # date-time = "time" +# Force the macros to use the `rust_decimal` crate for `NUMERIC`, even if `bigdecimal` is enabled. +# +# Defaults to "inferred": use whichever crate is enabled (`bigdecimal` takes precedence over `rust_decimal`). +numeric = "rust_decimal" +# Or, ensure the macros always prefer `bigdecimal` +# in case new decimal crates are added in the future: +# numeric = "bigdecimal" # Set global overrides for mapping SQL types to Rust types. # @@ -44,7 +51,9 @@ date-time = "chrono" # or not. They only override the inner type used. [macros.type-overrides] # Override a built-in type (map all `UUID` columns to `crate::types::MyUuid`) -'uuid' = "crate::types::MyUuid" +# Note: currently, the case of the type name MUST match. +# Built-in types are spelled in all-uppercase to match SQL convention. +'UUID' = "crate::types::MyUuid" # Support an external or custom wrapper type (e.g. from the `isn` Postgres extension) # (NOTE: FOR DOCUMENTATION PURPOSES ONLY; THIS CRATE/TYPE DOES NOT EXIST AS OF WRITING) diff --git a/sqlx-core/src/config/tests.rs b/sqlx-core/src/config/tests.rs index 6c2883d5..0b0b5909 100644 --- a/sqlx-core/src/config/tests.rs +++ b/sqlx-core/src/config/tests.rs @@ -20,9 +20,12 @@ fn assert_macros_config(config: &config::macros::Config) { use config::macros::*; assert_eq!(config.preferred_crates.date_time, DateTimeCrate::Chrono); + assert_eq!(config.preferred_crates.numeric, NumericCrate::RustDecimal); // Type overrides // Don't need to cover everything, just some important canaries. + assert_eq!(config.type_override("UUID"), Some("crate::types::MyUuid")); + assert_eq!(config.type_override("foo"), Some("crate::types::Foo")); assert_eq!(config.type_override(r#""Bar""#), Some("crate::types::Bar"),); @@ -79,6 +82,12 @@ fn assert_migrate_config(config: &config::migrate::Config) { assert_eq!(config.ignored_chars, ignored_chars); - assert_eq!(config.defaults.migration_type, DefaultMigrationType::Reversible); - assert_eq!(config.defaults.migration_versioning, DefaultVersioning::Sequential); + assert_eq!( + config.defaults.migration_type, + DefaultMigrationType::Reversible + ); + assert_eq!( + config.defaults.migration_versioning, + DefaultVersioning::Sequential + ); } diff --git a/sqlx-core/src/type_checking.rs b/sqlx-core/src/type_checking.rs index 384d15f4..d3d4a4c7 100644 --- a/sqlx-core/src/type_checking.rs +++ b/sqlx-core/src/type_checking.rs @@ -1,3 +1,4 @@ +use crate::config::macros::PreferredCrates; use crate::database::Database; use crate::decode::Decode; use crate::type_info::TypeInfo; @@ -26,12 +27,18 @@ pub trait TypeChecking: Database { /// /// If the type has a borrowed equivalent suitable for query parameters, /// this is that borrowed type. - fn param_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>; + fn param_type_for_id( + id: &Self::TypeInfo, + preferred_crates: &PreferredCrates, + ) -> Result<&'static str, Error>; /// Get the full path of the Rust type that corresponds to the given `TypeInfo`, if applicable. /// /// Always returns the owned version of the type, suitable for decoding from `Row`. - fn return_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>; + fn return_type_for_id( + id: &Self::TypeInfo, + preferred_crates: &PreferredCrates, + ) -> Result<&'static str, Error>; /// Get the name of the Cargo feature gate that must be enabled to process the given `TypeInfo`, /// if applicable. @@ -43,6 +50,18 @@ pub trait TypeChecking: Database { fn fmt_value_debug(value: &::Value) -> FmtValue<'_, Self>; } +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("no built-in mapping found for SQL type; a type override may be required")] + NoMappingFound, + #[error("Cargo feature for configured `macros.preferred-crates.date-time` not enabled")] + DateTimeCrateFeatureNotEnabled, + #[error("Cargo feature for configured `macros.preferred-crates.numeric` not enabled")] + NumericCrateFeatureNotEnabled, +} + /// An adapter for [`Value`] which attempts to decode the value and format it when printed using [`Debug`]. pub struct FmtValue<'v, DB> where @@ -134,36 +153,256 @@ macro_rules! impl_type_checking { }, ParamChecking::$param_checking:ident, feature-types: $ty_info:ident => $get_gate:expr, + datetime-types: { + chrono: { + $($chrono_ty:ty $(| $chrono_input:ty)?),*$(,)? + }, + time: { + $($time_ty:ty $(| $time_input:ty)?),*$(,)? + }, + }, + numeric-types: { + bigdecimal: { + $($bigdecimal_ty:ty $(| $bigdecimal_input:ty)?),*$(,)? + }, + rust_decimal: { + $($rust_decimal_ty:ty $(| $rust_decimal_input:ty)?),*$(,)? + }, + }, ) => { impl $crate::type_checking::TypeChecking for $database { const PARAM_CHECKING: $crate::type_checking::ParamChecking = $crate::type_checking::ParamChecking::$param_checking; - fn param_type_for_id(info: &Self::TypeInfo) -> Option<&'static str> { - match () { + fn param_type_for_id( + info: &Self::TypeInfo, + preferred_crates: &$crate::config::macros::PreferredCrates, + ) -> Result<&'static str, $crate::type_checking::Error> { + use $crate::config::macros::{DateTimeCrate, NumericCrate}; + use $crate::type_checking::Error; + + // Check `macros.preferred-crates.date-time` + // + // Due to legacy reasons, `time` takes precedent over `chrono` if both are enabled. + // Any crates added later should be _lower_ priority than `chrono` to avoid breakages. + // ---------------------------------------- + #[cfg(feature = "time")] + if matches!(preferred_crates.date_time, DateTimeCrate::Time | DateTimeCrate::Inferred) { $( - $(#[$meta])? - _ if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info => Some($crate::select_input_type!($ty $(, $input)?)), + if <$time_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok($crate::select_input_type!($time_ty $(, $time_input)?)); + } )* + $( - $(#[$meta])? - _ if <$ty as sqlx_core::types::Type<$database>>::compatible(info) => Some($crate::select_input_type!($ty $(, $input)?)), + if <$time_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok($crate::select_input_type!($time_ty $(, $time_input)?)); + } )* - _ => None } + + #[cfg(not(feature = "time"))] + if preferred_crates.date_time == DateTimeCrate::Time { + return Err(Error::DateTimeCrateFeatureNotEnabled); + } + + #[cfg(feature = "chrono")] + if matches!(preferred_crates.date_time, DateTimeCrate::Chrono | DateTimeCrate::Inferred) { + $( + if <$chrono_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok($crate::select_input_type!($chrono_ty $(, $chrono_input)?)); + } + )* + + $( + if <$chrono_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok($crate::select_input_type!($chrono_ty $(, $chrono_input)?)); + } + )* + } + + #[cfg(not(feature = "chrono"))] + if preferred_crates.date_time == DateTimeCrate::Chrono { + return Err(Error::DateTimeCrateFeatureNotEnabled); + } + + // Check `macros.preferred-crates.numeric` + // + // Due to legacy reasons, `bigdecimal` takes precedent over `rust_decimal` if + // both are enabled. + // ---------------------------------------- + #[cfg(feature = "bigdecimal")] + if matches!(preferred_crates.numeric, NumericCrate::BigDecimal | NumericCrate::Inferred) { + $( + if <$bigdecimal_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok($crate::select_input_type!($bigdecimal_ty $(, $bigdecimal_input)?)); + } + )* + + $( + if <$bigdecimal_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok($crate::select_input_type!($bigdecimal_ty $(, $bigdecimal_input)?)); + } + )* + } + + #[cfg(not(feature = "bigdecimal"))] + if preferred_crates.numeric == NumericCrate::BigDecimal { + return Err(Error::NumericCrateFeatureNotEnabled); + } + + #[cfg(feature = "rust_decimal")] + if matches!(preferred_crates.numeric, NumericCrate::RustDecimal | NumericCrate::Inferred) { + $( + if <$rust_decimal_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok($crate::select_input_type!($rust_decimal_ty $(, $rust_decimal_input)?)); + } + )* + + $( + if <$rust_decimal_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok($crate::select_input_type!($rust_decimal_ty $(, $rust_decimal_input)?)); + } + )* + } + + #[cfg(not(feature = "rust_decimal"))] + if preferred_crates.numeric == NumericCrate::RustDecimal { + return Err(Error::NumericCrateFeatureNotEnabled); + } + + // Check all other types + // --------------------- + $( + $(#[$meta])? + if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok($crate::select_input_type!($ty $(, $input)?)); + } + )* + + $( + $(#[$meta])? + if <$ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok($crate::select_input_type!($ty $(, $input)?)); + } + )* + + Err(Error::NoMappingFound) } - fn return_type_for_id(info: &Self::TypeInfo) -> Option<&'static str> { - match () { + fn return_type_for_id( + info: &Self::TypeInfo, + preferred_crates: &$crate::config::macros::PreferredCrates, + ) -> Result<&'static str, $crate::type_checking::Error> { + use $crate::config::macros::{DateTimeCrate, NumericCrate}; + use $crate::type_checking::Error; + + // Check `macros.preferred-crates.date-time` + // + // Due to legacy reasons, `time` takes precedent over `chrono` if both are enabled. + // Any crates added later should be _lower_ priority than `chrono` to avoid breakages. + // ---------------------------------------- + #[cfg(feature = "time")] + if matches!(preferred_crates.date_time, DateTimeCrate::Time | DateTimeCrate::Inferred) { $( - $(#[$meta])? - _ if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info => Some(stringify!($ty)), + if <$time_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok(stringify!($time_ty)); + } )* + $( - $(#[$meta])? - _ if <$ty as sqlx_core::types::Type<$database>>::compatible(info) => Some(stringify!($ty)), + if <$time_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok(stringify!($time_ty)); + } )* - _ => None } + + #[cfg(not(feature = "time"))] + if preferred_crates.date_time == DateTimeCrate::Time { + return Err(Error::DateTimeCrateFeatureNotEnabled); + } + + #[cfg(feature = "chrono")] + if matches!(preferred_crates.date_time, DateTimeCrate::Chrono | DateTimeCrate::Inferred) { + $( + if <$chrono_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok(stringify!($chrono_ty)); + } + )* + + $( + if <$chrono_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok(stringify!($chrono_ty)); + } + )* + } + + #[cfg(not(feature = "chrono"))] + if preferred_crates.date_time == DateTimeCrate::Chrono { + return Err(Error::DateTimeCrateFeatureNotEnabled); + } + + // Check `macros.preferred-crates.numeric` + // + // Due to legacy reasons, `bigdecimal` takes precedent over `rust_decimal` if + // both are enabled. + // ---------------------------------------- + #[cfg(feature = "bigdecimal")] + if matches!(preferred_crates.numeric, NumericCrate::BigDecimal | NumericCrate::Inferred) { + $( + if <$bigdecimal_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok(stringify!($bigdecimal_ty)); + } + )* + + $( + if <$bigdecimal_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok(stringify!($bigdecimal_ty)); + } + )* + } + + #[cfg(not(feature = "bigdecimal"))] + if preferred_crates.numeric == NumericCrate::BigDecimal { + return Err(Error::NumericCrateFeatureNotEnabled); + } + + #[cfg(feature = "rust_decimal")] + if matches!(preferred_crates.numeric, NumericCrate::RustDecimal | NumericCrate::Inferred) { + $( + if <$rust_decimal_ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok($crate::select_input_type!($rust_decimal_ty $(, $rust_decimal_input)?)); + } + )* + + $( + if <$rust_decimal_ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok($crate::select_input_type!($rust_decimal_ty $(, $rust_decimal_input)?)); + } + )* + } + + #[cfg(not(feature = "rust_decimal"))] + if preferred_crates.numeric == NumericCrate::RustDecimal { + return Err(Error::NumericCrateFeatureNotEnabled); + } + + // Check all other types + // --------------------- + $( + $(#[$meta])? + if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info { + return Ok(stringify!($ty)); + } + )* + + $( + $(#[$meta])? + if <$ty as sqlx_core::types::Type<$database>>::compatible(info) { + return Ok(stringify!($ty)); + } + )* + + Err(Error::NoMappingFound) } fn get_feature_gate($ty_info: &Self::TypeInfo) -> Option<&'static str> { @@ -175,13 +414,32 @@ macro_rules! impl_type_checking { let info = value.type_info(); - match () { + #[cfg(feature = "time")] + { $( - $(#[$meta])? - _ if <$ty as sqlx_core::types::Type<$database>>::compatible(&info) => $crate::type_checking::FmtValue::debug::<$ty>(value), + if <$time_ty as sqlx_core::types::Type<$database>>::compatible(&info) { + return $crate::type_checking::FmtValue::debug::<$time_ty>(value); + } )* - _ => $crate::type_checking::FmtValue::unknown(value), } + + #[cfg(feature = "chrono")] + { + $( + if <$chrono_ty as sqlx_core::types::Type<$database>>::compatible(&info) { + return $crate::type_checking::FmtValue::debug::<$chrono_ty>(value); + } + )* + } + + $( + $(#[$meta])? + if <$ty as sqlx_core::types::Type<$database>>::compatible(&info) { + return $crate::type_checking::FmtValue::debug::<$ty>(value); + } + )* + + $crate::type_checking::FmtValue::unknown(value) } } }; diff --git a/sqlx-macros-core/src/query/args.rs b/sqlx-macros-core/src/query/args.rs index ec17aeff..1ddc5e98 100644 --- a/sqlx-macros-core/src/query/args.rs +++ b/sqlx-macros-core/src/query/args.rs @@ -3,7 +3,10 @@ use crate::query::QueryMacroInput; use either::Either; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned}; +use sqlx_core::config::Config; use sqlx_core::describe::Describe; +use sqlx_core::type_checking; +use sqlx_core::type_info::TypeInfo; use syn::spanned::Spanned; use syn::{Expr, ExprCast, ExprGroup, Type}; @@ -11,6 +14,7 @@ use syn::{Expr, ExprCast, ExprGroup, Type}; /// and binds them to `DB::Arguments` with the ident `query_args`. pub fn quote_args( input: &QueryMacroInput, + config: &Config, info: &Describe, ) -> crate::Result { let db_path = DB::db_path(); @@ -55,22 +59,7 @@ pub fn quote_args( return Ok(quote!()); } - let param_ty = - DB::param_type_for_id(param_ty) - .ok_or_else(|| { - if let Some(feature_gate) = DB::get_feature_gate(param_ty) { - format!( - "optional sqlx feature `{}` required for type {} of param #{}", - feature_gate, - param_ty, - i + 1, - ) - } else { - format!("unsupported type {} for param #{}", param_ty, i + 1) - } - })? - .parse::() - .map_err(|_| format!("Rust type mapping for {param_ty} not parsable"))?; + let param_ty = get_param_type::(param_ty, config, i)?; Ok(quote_spanned!(expr.span() => // this shouldn't actually run @@ -115,6 +104,63 @@ pub fn quote_args( }) } +fn get_param_type( + param_ty: &DB::TypeInfo, + config: &Config, + i: usize, +) -> crate::Result { + if let Some(type_override) = config.macros.type_override(param_ty.name()) { + return Ok(type_override.parse()?); + } + + let err = match DB::param_type_for_id(param_ty, &config.macros.preferred_crates) { + Ok(t) => return Ok(t.parse()?), + Err(e) => e, + }; + + let param_num = i + 1; + + let message = match err { + type_checking::Error::NoMappingFound => { + if let Some(feature_gate) = DB::get_feature_gate(param_ty) { + format!( + "optional sqlx feature `{feature_gate}` required for type {param_ty} of param #{param_num}", + ) + } else { + format!("unsupported type {param_ty} for param #{param_num}") + } + } + type_checking::Error::DateTimeCrateFeatureNotEnabled => { + let feature_gate = config + .macros + .preferred_crates + .date_time + .crate_name() + .expect("BUG: got feature-not-enabled error for DateTimeCrate::Inferred"); + + format!( + "SQLx feature `{feature_gate}` required for type {param_ty} of param #{param_num} \ + (configured by `macros.preferred-crates.date-time` in sqlx.toml)", + ) + } + type_checking::Error::NumericCrateFeatureNotEnabled => { + let feature_gate = config + .macros + .preferred_crates + .numeric + .crate_name() + .expect("BUG: got feature-not-enabled error for NumericCrate::Inferred"); + + format!( + "SQLx feature `{feature_gate}` required for type {param_ty} of param #{param_num} \ + (configured by `macros.preferred-crates.numeric` in sqlx.toml)", + ) + } + }; + + Err(message.into()) +} + fn get_type_override(expr: &Expr) -> Option<&Type> { match expr { Expr::Group(group) => get_type_override(&group.expr), diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index 190d272d..37592d4f 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -15,8 +15,8 @@ use crate::database::DatabaseExt; use crate::query::data::{hash_string, DynQueryData, QueryData}; use crate::query::input::RecordType; use either::Either; -use url::Url; use sqlx_core::config::Config; +use url::Url; mod args; mod data; @@ -139,11 +139,9 @@ static METADATA: Lazy = Lazy::new(|| { let offline = env("SQLX_OFFLINE") .map(|s| s.eq_ignore_ascii_case("true") || s == "1") .unwrap_or(false); - - let var_name = Config::from_crate() - .common - .database_url_var(); - + + let var_name = Config::from_crate().common.database_url_var(); + let database_url = env(var_name).ok(); Metadata { @@ -251,6 +249,8 @@ fn expand_with_data( where Describe: DescribeExt, { + let config = Config::from_crate(); + // validate at the minimum that our args match the query's input parameters let num_parameters = match data.describe.parameters() { Some(Either::Left(params)) => Some(params.len()), @@ -267,7 +267,7 @@ where } } - let args_tokens = args::quote_args(&input, &data.describe)?; + let args_tokens = args::quote_args(&input, config, &data.describe)?; let query_args = format_ident!("query_args"); @@ -286,7 +286,7 @@ where } else { match input.record_type { RecordType::Generated => { - let columns = output::columns_to_rust::(&data.describe)?; + let columns = output::columns_to_rust::(&data.describe, config)?; let record_name: Type = syn::parse_str("Record").unwrap(); @@ -322,22 +322,40 @@ where record_tokens } RecordType::Given(ref out_ty) => { - let columns = output::columns_to_rust::(&data.describe)?; + let columns = output::columns_to_rust::(&data.describe, config)?; output::quote_query_as::(&input, out_ty, &query_args, &columns) } RecordType::Scalar => { - output::quote_query_scalar::(&input, &query_args, &data.describe)? + output::quote_query_scalar::(&input, config, &query_args, &data.describe)? } } }; + let mut warnings = TokenStream::new(); + + if config.macros.preferred_crates.date_time.is_inferred() { + // Warns if the date-time crate is inferred but both `chrono` and `time` are enabled + warnings.extend(quote! { + ::sqlx::warn_on_ambiguous_inferred_date_time_crate(); + }); + } + + if config.macros.preferred_crates.numeric.is_inferred() { + // Warns if the numeric crate is inferred but both `bigdecimal` and `rust_decimal` are enabled + warnings.extend(quote! { + ::sqlx::warn_on_ambiguous_inferred_numeric_crate(); + }); + } + let ret_tokens = quote! { { #[allow(clippy::all)] { use ::sqlx::Arguments as _; + #warnings + #args_tokens #output diff --git a/sqlx-macros-core/src/query/output.rs b/sqlx-macros-core/src/query/output.rs index d9dc79a3..1a145e3a 100644 --- a/sqlx-macros-core/src/query/output.rs +++ b/sqlx-macros-core/src/query/output.rs @@ -8,12 +8,13 @@ use sqlx_core::describe::Describe; use crate::database::DatabaseExt; use crate::query::QueryMacroInput; +use sqlx_core::config::Config; +use sqlx_core::type_checking; use sqlx_core::type_checking::TypeChecking; +use sqlx_core::type_info::TypeInfo; use std::fmt::{self, Display, Formatter}; use syn::parse::{Parse, ParseStream}; use syn::Token; -use sqlx_core::config::Config; -use sqlx_core::type_info::TypeInfo; pub struct RustColumn { pub(super) ident: Ident, @@ -78,13 +79,20 @@ impl Display for DisplayColumn<'_> { } } -pub fn columns_to_rust(describe: &Describe) -> crate::Result> { +pub fn columns_to_rust( + describe: &Describe, + config: &Config, +) -> crate::Result> { (0..describe.columns().len()) - .map(|i| column_to_rust(describe, i)) + .map(|i| column_to_rust(describe, config, i)) .collect::>>() } -fn column_to_rust(describe: &Describe, i: usize) -> crate::Result { +fn column_to_rust( + describe: &Describe, + config: &Config, + i: usize, +) -> crate::Result { let column = &describe.columns()[i]; // add raw prefix to all identifiers @@ -108,7 +116,7 @@ fn column_to_rust(describe: &Describe, i: usize) -> crate:: (ColumnTypeOverride::Wildcard, true) => ColumnType::OptWildcard, (ColumnTypeOverride::None, _) => { - let type_ = get_column_type::(i, column); + let type_ = get_column_type::(config, i, column); if !nullable { ColumnType::Exact(type_) } else { @@ -195,6 +203,7 @@ pub fn quote_query_as( pub fn quote_query_scalar( input: &QueryMacroInput, + config: &Config, bind_args: &Ident, describe: &Describe, ) -> crate::Result { @@ -209,10 +218,10 @@ pub fn quote_query_scalar( } // attempt to parse a column override, otherwise fall back to the inferred type of the column - let ty = if let Ok(rust_col) = column_to_rust(describe, 0) { + let ty = if let Ok(rust_col) = column_to_rust(describe, config, 0) { rust_col.type_.to_token_stream() } else if input.checked { - let ty = get_column_type::(0, &columns[0]); + let ty = get_column_type::(config, 0, &columns[0]); if describe.nullable(0).unwrap_or(true) { quote! { ::std::option::Option<#ty> } } else { @@ -230,52 +239,92 @@ pub fn quote_query_scalar( }) } -fn get_column_type(i: usize, column: &DB::Column) -> TokenStream { +fn get_column_type(config: &Config, i: usize, column: &DB::Column) -> TokenStream { if let ColumnOrigin::Table(origin) = column.origin() { - if let Some(column_override) = Config::from_crate() - .macros - .column_override(&origin.table, &origin.name) - { + if let Some(column_override) = config.macros.column_override(&origin.table, &origin.name) { return column_override.parse().unwrap(); } } - + let type_info = column.type_info(); - if let Some(type_override) = Config::from_crate() - .macros - .type_override(type_info.name()) - { - return type_override.parse().unwrap(); + if let Some(type_override) = config.macros.type_override(type_info.name()) { + return type_override.parse().unwrap(); } - - ::return_type_for_id(type_info).map_or_else( - || { - let message = - if let Some(feature_gate) = ::get_feature_gate(type_info) { - format!( - "optional sqlx feature `{feat}` required for type {ty} of {col}", - ty = &type_info, - feat = feature_gate, - col = DisplayColumn { - idx: i, - name: column.name() - } - ) - } else { - format!( - "unsupported type {ty} of {col}", - ty = type_info, - col = DisplayColumn { - idx: i, - name: column.name() - } - ) - }; - syn::Error::new(Span::call_site(), message).to_compile_error() - }, - |t| t.parse().unwrap(), - ) + + let err = match ::return_type_for_id( + type_info, + &config.macros.preferred_crates, + ) { + Ok(t) => return t.parse().unwrap(), + Err(e) => e, + }; + + let message = match err { + type_checking::Error::NoMappingFound => { + if let Some(feature_gate) = ::get_feature_gate(type_info) { + format!( + "SQLx feature `{feat}` required for type {ty} of {col}", + ty = &type_info, + feat = feature_gate, + col = DisplayColumn { + idx: i, + name: column.name() + } + ) + } else { + format!( + "no built-in mapping found for type {ty} of {col}; \ + a type override may be required, see documentation for details", + ty = type_info, + col = DisplayColumn { + idx: i, + name: column.name() + } + ) + } + } + type_checking::Error::DateTimeCrateFeatureNotEnabled => { + let feature_gate = config + .macros + .preferred_crates + .date_time + .crate_name() + .expect("BUG: got feature-not-enabled error for DateTimeCrate::Inferred"); + + format!( + "SQLx feature `{feat}` required for type {ty} of {col} \ + (configured by `macros.preferred-crates.date-time` in sqlx.toml)", + ty = &type_info, + feat = feature_gate, + col = DisplayColumn { + idx: i, + name: column.name() + } + ) + } + type_checking::Error::NumericCrateFeatureNotEnabled => { + let feature_gate = config + .macros + .preferred_crates + .numeric + .crate_name() + .expect("BUG: got feature-not-enabled error for NumericCrate::Inferred"); + + format!( + "SQLx feature `{feat}` required for type {ty} of {col} \ + (configured by `macros.preferred-crates.numeric` in sqlx.toml)", + ty = &type_info, + feat = feature_gate, + col = DisplayColumn { + idx: i, + name: column.name() + } + ) + } + }; + + syn::Error::new(Span::call_site(), message).to_compile_error() } impl ColumnDecl { diff --git a/sqlx-mysql/src/connection/executor.rs b/sqlx-mysql/src/connection/executor.rs index 6baad5cc..d0d9cf18 100644 --- a/sqlx-mysql/src/connection/executor.rs +++ b/sqlx-mysql/src/connection/executor.rs @@ -22,8 +22,8 @@ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_core::Stream; use futures_util::{pin_mut, TryStreamExt}; -use std::{borrow::Cow, sync::Arc}; use sqlx_core::column::{ColumnOrigin, TableColumn}; +use std::{borrow::Cow, sync::Arc}; impl MySqlConnection { async fn prepare_statement<'c>( @@ -391,7 +391,7 @@ fn recv_next_result_column(def: &ColumnDefinition, ordinal: usize) -> Result Result Result<&str, Error> { str::from_utf8(&self.table).map_err(Error::protocol) } - + pub(crate) fn name(&self) -> Result<&str, Error> { str::from_utf8(&self.name).map_err(Error::protocol) } diff --git a/sqlx-mysql/src/type_checking.rs b/sqlx-mysql/src/type_checking.rs index 3f3ce583..0bdc84d8 100644 --- a/sqlx-mysql/src/type_checking.rs +++ b/sqlx-mysql/src/type_checking.rs @@ -25,41 +25,39 @@ impl_type_checking!( // BINARY, VAR_BINARY, BLOB Vec, - // Types from third-party crates need to be referenced at a known path - // for the macros to work, but we don't want to require the user to add extra dependencies. - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveTime, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveDate, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveDateTime, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::DateTime, - - #[cfg(feature = "time")] - sqlx::types::time::Time, - - #[cfg(feature = "time")] - sqlx::types::time::Date, - - #[cfg(feature = "time")] - sqlx::types::time::PrimitiveDateTime, - - #[cfg(feature = "time")] - sqlx::types::time::OffsetDateTime, - - #[cfg(feature = "bigdecimal")] - sqlx::types::BigDecimal, - - #[cfg(feature = "rust_decimal")] - sqlx::types::Decimal, - #[cfg(feature = "json")] sqlx::types::JsonValue, }, ParamChecking::Weak, feature-types: info => info.__type_feature_gate(), + // The expansion of the macro automatically applies the correct feature name + // and checks `[macros.preferred-crates]` + datetime-types: { + chrono: { + sqlx::types::chrono::NaiveTime, + + sqlx::types::chrono::NaiveDate, + + sqlx::types::chrono::NaiveDateTime, + + sqlx::types::chrono::DateTime, + }, + time: { + sqlx::types::time::Time, + + sqlx::types::time::Date, + + sqlx::types::time::PrimitiveDateTime, + + sqlx::types::time::OffsetDateTime, + }, + }, + numeric-types: { + bigdecimal: { + sqlx::types::BigDecimal, + }, + rust_decimal: { + sqlx::types::Decimal, + }, + }, ); diff --git a/sqlx-postgres/src/column.rs b/sqlx-postgres/src/column.rs index bd08e27d..4dd3a1cb 100644 --- a/sqlx-postgres/src/column.rs +++ b/sqlx-postgres/src/column.rs @@ -1,8 +1,8 @@ use crate::ext::ustr::UStr; use crate::{PgTypeInfo, Postgres}; -pub(crate) use sqlx_core::column::{Column, ColumnIndex}; use sqlx_core::column::ColumnOrigin; +pub(crate) use sqlx_core::column::{Column, ColumnIndex}; #[derive(Debug, Clone)] #[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))] @@ -13,7 +13,7 @@ pub struct PgColumn { #[cfg_attr(feature = "offline", serde(default))] pub(crate) origin: ColumnOrigin, - + #[cfg_attr(feature = "offline", serde(skip))] pub(crate) relation_id: Option, #[cfg_attr(feature = "offline", serde(skip))] diff --git a/sqlx-postgres/src/connection/describe.rs b/sqlx-postgres/src/connection/describe.rs index 53affe5d..4decdde5 100644 --- a/sqlx-postgres/src/connection/describe.rs +++ b/sqlx-postgres/src/connection/describe.rs @@ -1,4 +1,4 @@ -use std::collections::btree_map; +use crate::connection::TableColumns; use crate::error::Error; use crate::ext::ustr::UStr; use crate::io::StatementId; @@ -12,11 +12,9 @@ use crate::types::Oid; use crate::HashMap; use crate::{PgColumn, PgConnection, PgTypeInfo}; use smallvec::SmallVec; +use sqlx_core::column::{ColumnOrigin, TableColumn}; use sqlx_core::query_builder::QueryBuilder; use std::sync::Arc; -use sqlx_core::column::{ColumnOrigin, TableColumn}; -use sqlx_core::hash_map; -use crate::connection::TableColumns; /// Describes the type of the `pg_type.typtype` column /// @@ -125,9 +123,12 @@ impl PgConnection { let type_info = self .maybe_fetch_type_info_by_oid(field.data_type_id, should_fetch) .await?; - - let origin = if let (Some(relation_oid), Some(attribute_no)) = (field.relation_id, field.relation_attribute_no) { - self.maybe_fetch_column_origin(relation_oid, attribute_no, should_fetch).await? + + let origin = if let (Some(relation_oid), Some(attribute_no)) = + (field.relation_id, field.relation_attribute_no) + { + self.maybe_fetch_column_origin(relation_oid, attribute_no, should_fetch) + .await? } else { ColumnOrigin::Expression }; @@ -200,52 +201,65 @@ impl PgConnection { Ok(PgTypeInfo(PgType::DeclareWithOid(oid))) } } - + async fn maybe_fetch_column_origin( - &mut self, - relation_id: Oid, + &mut self, + relation_id: Oid, attribute_no: i16, should_fetch: bool, ) -> Result { - let mut table_columns = match self.cache_table_to_column_names.entry(relation_id) { - hash_map::Entry::Occupied(table_columns) => { - table_columns.into_mut() - }, - hash_map::Entry::Vacant(vacant) => { - if !should_fetch { return Ok(ColumnOrigin::Unknown); } - - let table_name: String = query_scalar("SELECT $1::oid::regclass::text") - .bind(relation_id) - .fetch_one(&mut *self) - .await?; - - vacant.insert(TableColumns { - table_name: table_name.into(), - columns: Default::default(), + if let Some(origin) = + self.cache_table_to_column_names + .get(&relation_id) + .and_then(|table_columns| { + let column_name = table_columns.columns.get(&attribute_no).cloned()?; + + Some(ColumnOrigin::Table(TableColumn { + table: table_columns.table_name.clone(), + name: column_name, + })) }) - } + { + return Ok(origin); + } + + if !should_fetch { + return Ok(ColumnOrigin::Unknown); + } + + // Looking up the table name _may_ end up being redundant, + // but the round-trip to the server is by far the most expensive part anyway. + let Some((table_name, column_name)): Option<(String, String)> = query_as( + // language=PostgreSQL + "SELECT $1::oid::regclass::text, attname \ + FROM pg_catalog.pg_attribute \ + WHERE attrelid = $1 AND attnum = $2", + ) + .bind(relation_id) + .bind(attribute_no) + .fetch_optional(&mut *self) + .await? + else { + // The column/table doesn't exist anymore for whatever reason. + return Ok(ColumnOrigin::Unknown); }; - - let column_name = match table_columns.columns.entry(attribute_no) { - btree_map::Entry::Occupied(occupied) => Arc::clone(occupied.get()), - btree_map::Entry::Vacant(vacant) => { - if !should_fetch { return Ok(ColumnOrigin::Unknown); } - - let column_name: String = query_scalar( - "SELECT attname FROM pg_attribute WHERE attrelid = $1 AND attnum = $2" - ) - .bind(relation_id) - .bind(attribute_no) - .fetch_one(&mut *self) - .await?; - - Arc::clone(vacant.insert(column_name.into())) - } - }; - + + let table_columns = self + .cache_table_to_column_names + .entry(relation_id) + .or_insert_with(|| TableColumns { + table_name: table_name.into(), + columns: Default::default(), + }); + + let column_name = table_columns + .columns + .entry(attribute_no) + .or_insert(column_name.into()); + Ok(ColumnOrigin::Table(TableColumn { table: table_columns.table_name.clone(), - name: column_name + name: Arc::clone(column_name), })) } diff --git a/sqlx-postgres/src/connection/establish.rs b/sqlx-postgres/src/connection/establish.rs index 1bc4172f..684bf265 100644 --- a/sqlx-postgres/src/connection/establish.rs +++ b/sqlx-postgres/src/connection/establish.rs @@ -148,8 +148,8 @@ impl PgConnection { cache_type_oid: HashMap::new(), cache_type_info: HashMap::new(), cache_elem_type_to_array: HashMap::new(), - log_settings: options.log_settings.clone(), - }), + cache_table_to_column_names: HashMap::new(), + log_settings: options.log_settings.clone(),}), }) } } diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index eb18c5a9..41661a84 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -39,42 +39,6 @@ impl_type_checking!( #[cfg(feature = "uuid")] sqlx::types::Uuid, - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveTime, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveDate, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveDateTime, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::DateTime | sqlx::types::chrono::DateTime<_>, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::postgres::types::PgTimeTz, - - #[cfg(feature = "time")] - sqlx::types::time::Time, - - #[cfg(feature = "time")] - sqlx::types::time::Date, - - #[cfg(feature = "time")] - sqlx::types::time::PrimitiveDateTime, - - #[cfg(feature = "time")] - sqlx::types::time::OffsetDateTime, - - #[cfg(feature = "time")] - sqlx::postgres::types::PgTimeTz, - - #[cfg(feature = "bigdecimal")] - sqlx::types::BigDecimal, - - #[cfg(feature = "rust_decimal")] - sqlx::types::Decimal, - #[cfg(feature = "ipnetwork")] sqlx::types::ipnetwork::IpNetwork, @@ -106,36 +70,6 @@ impl_type_checking!( #[cfg(feature = "uuid")] Vec | &[sqlx::types::Uuid], - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec | &[sqlx::types::chrono::NaiveTime], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec | &[sqlx::types::chrono::NaiveDate], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec | &[sqlx::types::chrono::NaiveDateTime], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec> | &[sqlx::types::chrono::DateTime<_>], - - #[cfg(feature = "time")] - Vec | &[sqlx::types::time::Time], - - #[cfg(feature = "time")] - Vec | &[sqlx::types::time::Date], - - #[cfg(feature = "time")] - Vec | &[sqlx::types::time::PrimitiveDateTime], - - #[cfg(feature = "time")] - Vec | &[sqlx::types::time::OffsetDateTime], - - #[cfg(feature = "bigdecimal")] - Vec | &[sqlx::types::BigDecimal], - - #[cfg(feature = "rust_decimal")] - Vec | &[sqlx::types::Decimal], - #[cfg(feature = "ipnetwork")] Vec | &[sqlx::types::ipnetwork::IpNetwork], @@ -152,72 +86,114 @@ impl_type_checking!( sqlx::postgres::types::PgRange, sqlx::postgres::types::PgRange, - #[cfg(feature = "bigdecimal")] - sqlx::postgres::types::PgRange, - - #[cfg(feature = "rust_decimal")] - sqlx::postgres::types::PgRange, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::postgres::types::PgRange, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::postgres::types::PgRange, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::postgres::types::PgRange> | - sqlx::postgres::types::PgRange>, - - #[cfg(feature = "time")] - sqlx::postgres::types::PgRange, - - #[cfg(feature = "time")] - sqlx::postgres::types::PgRange, - - #[cfg(feature = "time")] - sqlx::postgres::types::PgRange, - // Range arrays Vec> | &[sqlx::postgres::types::PgRange], Vec> | &[sqlx::postgres::types::PgRange], - - #[cfg(feature = "bigdecimal")] - Vec> | - &[sqlx::postgres::types::PgRange], - - #[cfg(feature = "rust_decimal")] - Vec> | - &[sqlx::postgres::types::PgRange], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec> | - &[sqlx::postgres::types::PgRange], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec> | - &[sqlx::postgres::types::PgRange], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec>> | - &[sqlx::postgres::types::PgRange>], - - #[cfg(all(feature = "chrono", not(feature = "time")))] - Vec>> | - &[sqlx::postgres::types::PgRange>], - - #[cfg(feature = "time")] - Vec> | - &[sqlx::postgres::types::PgRange], - - #[cfg(feature = "time")] - Vec> | - &[sqlx::postgres::types::PgRange], - - #[cfg(feature = "time")] - Vec> | - &[sqlx::postgres::types::PgRange], }, ParamChecking::Strong, feature-types: info => info.__type_feature_gate(), + // The expansion of the macro automatically applies the correct feature name + // and checks `[macros.preferred-crates]` + datetime-types: { + chrono: { + // Scalar types + sqlx::types::chrono::NaiveTime, + + sqlx::types::chrono::NaiveDate, + + sqlx::types::chrono::NaiveDateTime, + + sqlx::types::chrono::DateTime | sqlx::types::chrono::DateTime<_>, + + sqlx::postgres::types::PgTimeTz, + + // Array types + Vec | &[sqlx::types::chrono::NaiveTime], + + Vec | &[sqlx::types::chrono::NaiveDate], + + Vec | &[sqlx::types::chrono::NaiveDateTime], + + Vec> | &[sqlx::types::chrono::DateTime<_>], + + // Range types + sqlx::postgres::types::PgRange, + + sqlx::postgres::types::PgRange, + + sqlx::postgres::types::PgRange> | + sqlx::postgres::types::PgRange>, + + // Arrays of ranges + Vec> | + &[sqlx::postgres::types::PgRange], + + Vec> | + &[sqlx::postgres::types::PgRange], + + Vec>> | + &[sqlx::postgres::types::PgRange>], + }, + time: { + // Scalar types + sqlx::types::time::Time, + + sqlx::types::time::Date, + + sqlx::types::time::PrimitiveDateTime, + + sqlx::types::time::OffsetDateTime, + + sqlx::postgres::types::PgTimeTz, + + // Array types + Vec | &[sqlx::types::time::Time], + + Vec | &[sqlx::types::time::Date], + + Vec | &[sqlx::types::time::PrimitiveDateTime], + + Vec | &[sqlx::types::time::OffsetDateTime], + + // Range types + sqlx::postgres::types::PgRange, + + sqlx::postgres::types::PgRange, + + sqlx::postgres::types::PgRange, + + // Arrays of ranges + Vec> | + &[sqlx::postgres::types::PgRange], + + Vec> | + &[sqlx::postgres::types::PgRange], + + Vec> | + &[sqlx::postgres::types::PgRange], + }, + }, + numeric-types: { + bigdecimal: { + sqlx::types::BigDecimal, + + Vec | &[sqlx::types::BigDecimal], + + sqlx::postgres::types::PgRange, + + Vec> | + &[sqlx::postgres::types::PgRange], + }, + rust_decimal: { + sqlx::types::Decimal, + + Vec | &[sqlx::types::Decimal], + + sqlx::postgres::types::PgRange, + + Vec> | + &[sqlx::postgres::types::PgRange], + }, + }, ); diff --git a/sqlx-sqlite/src/column.rs b/sqlx-sqlite/src/column.rs index 390f3687..d319bd46 100644 --- a/sqlx-sqlite/src/column.rs +++ b/sqlx-sqlite/src/column.rs @@ -11,7 +11,7 @@ pub struct SqliteColumn { pub(crate) type_info: SqliteTypeInfo, #[cfg_attr(feature = "offline", serde(default))] - pub(crate) origin: ColumnOrigin + pub(crate) origin: ColumnOrigin, } impl Column for SqliteColumn { diff --git a/sqlx-sqlite/src/connection/describe.rs b/sqlx-sqlite/src/connection/describe.rs index 9ba9f8c3..6db81374 100644 --- a/sqlx-sqlite/src/connection/describe.rs +++ b/sqlx-sqlite/src/connection/describe.rs @@ -49,7 +49,7 @@ pub(crate) fn describe(conn: &mut ConnectionState, query: &str) -> Result ColumnOrigin { - if let Some((table, name)) = - self.column_table_name(index).zip(self.column_origin_name(index)) + if let Some((table, name)) = self + .column_table_name(index) + .zip(self.column_origin_name(index)) { let table: Arc = self .column_db_name(index) @@ -125,20 +126,20 @@ impl StatementHandle { // TODO: check that SQLite returns the names properly quoted if necessary |db| format!("{db}.{table}").into(), ); - + ColumnOrigin::Table(TableColumn { table, - name: name.into() + name: name.into(), }) } else { ColumnOrigin::Expression } } - + fn column_db_name(&self, index: usize) -> Option<&str> { unsafe { let db_name = sqlite3_column_database_name(self.0.as_ptr(), check_col_idx!(index)); - + if !db_name.is_null() { Some(from_utf8_unchecked(CStr::from_ptr(db_name).to_bytes())) } else { @@ -170,7 +171,7 @@ impl StatementHandle { } } } - + pub(crate) fn column_type_info(&self, index: usize) -> SqliteTypeInfo { SqliteTypeInfo(DataType::from_code(self.column_type(index))) } diff --git a/sqlx-sqlite/src/statement/virtual.rs b/sqlx-sqlite/src/statement/virtual.rs index 6be980c3..345af307 100644 --- a/sqlx-sqlite/src/statement/virtual.rs +++ b/sqlx-sqlite/src/statement/virtual.rs @@ -104,6 +104,7 @@ impl VirtualStatement { ordinal: i, name: name.clone(), type_info, + origin: statement.column_origin(i), }); column_names.insert(name, i); diff --git a/sqlx-sqlite/src/type_checking.rs b/sqlx-sqlite/src/type_checking.rs index e1ac3bc7..97af601c 100644 --- a/sqlx-sqlite/src/type_checking.rs +++ b/sqlx-sqlite/src/type_checking.rs @@ -1,8 +1,7 @@ +use crate::Sqlite; #[allow(unused_imports)] use sqlx_core as sqlx; -use crate::Sqlite; - // f32 is not included below as REAL represents a floating point value // stored as an 8-byte IEEE floating point number (i.e. an f64) // For more info see: https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes @@ -20,24 +19,6 @@ impl_type_checking!( String, Vec, - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveDate, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::NaiveDateTime, - - #[cfg(all(feature = "chrono", not(feature = "time")))] - sqlx::types::chrono::DateTime | sqlx::types::chrono::DateTime<_>, - - #[cfg(feature = "time")] - sqlx::types::time::OffsetDateTime, - - #[cfg(feature = "time")] - sqlx::types::time::PrimitiveDateTime, - - #[cfg(feature = "time")] - sqlx::types::time::Date, - #[cfg(feature = "uuid")] sqlx::types::Uuid, }, @@ -48,4 +29,28 @@ impl_type_checking!( // The type integrations simply allow the user to skip some intermediate representation, // which is usually TEXT. feature-types: _info => None, + + // The expansion of the macro automatically applies the correct feature name + // and checks `[macros.preferred-crates]` + datetime-types: { + chrono: { + sqlx::types::chrono::NaiveDate, + + sqlx::types::chrono::NaiveDateTime, + + sqlx::types::chrono::DateTime + | sqlx::types::chrono::DateTime<_>, + }, + time: { + sqlx::types::time::OffsetDateTime, + + sqlx::types::time::PrimitiveDateTime, + + sqlx::types::time::Date, + }, + }, + numeric-types: { + bigdecimal: { }, + rust_decimal: { }, + }, ); diff --git a/src/lib.rs b/src/lib.rs index a357753b..7c10b852 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,3 +168,27 @@ pub mod prelude { #[cfg(feature = "_unstable-doc")] pub use sqlx_core::config; + +#[doc(hidden)] +#[cfg_attr( + all(feature = "chrono", feature = "time"), + deprecated = "SQLx has both `chrono` and `time` features enabled, \ + which presents an ambiguity when the `query!()` macros are mapping date/time types. \ + The `query!()` macros prefer types from `time` by default, \ + but this behavior should not be relied upon; \ + to resolve the ambiguity, we recommend specifying the preferred crate in a `sqlx.toml` file: \ + https://docs.rs/sqlx/latest/sqlx/config/macros/PreferredCrates.html#field.date_time" +)] +pub fn warn_on_ambiguous_inferred_date_time_crate() {} + +#[doc(hidden)] +#[cfg_attr( + all(feature = "bigdecimal", feature = "rust_decimal"), + deprecated = "SQLx has both `bigdecimal` and `rust_decimal` features enabled, \ + which presents an ambiguity when the `query!()` macros are mapping `NUMERIC`. \ + The `query!()` macros prefer `bigdecimal::BigDecimal` by default, \ + but this behavior should not be relied upon; \ + to resolve the ambiguity, we recommend specifying the preferred crate in a `sqlx.toml` file: \ + https://docs.rs/sqlx/latest/sqlx/config/macros/PreferredCrates.html#field.numeric" +)] +pub fn warn_on_ambiguous_inferred_numeric_crate() {}