diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index 5f2d9a655..21f8b7ea4 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -352,8 +352,8 @@ impl ActiveEnum { #[automatically_derived] impl sea_orm::TryGetable for #ident { - fn try_get(res: &sea_orm::QueryResult, pre: &str, col: &str) -> std::result::Result { - let value = <::Value as sea_orm::TryGetable>::try_get(res, pre, col)?; + fn try_get_by(res: &sea_orm::QueryResult, idx: I) -> std::result::Result { + let value = <::Value as sea_orm::TryGetable>::try_get_by(res, idx)?; ::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr) } } diff --git a/src/database/mock.rs b/src/database/mock.rs index 4a3640abd..734ed39c0 100644 --- a/src/database/mock.rs +++ b/src/database/mock.rs @@ -203,12 +203,25 @@ impl MockDatabaseTrait for MockDatabase { } impl MockRow { - /// Try to get the values of a [MockRow] and fail gracefully on error - pub fn try_get(&self, col: &str) -> Result + /// Get a value from the [MockRow] + pub fn try_get(&self, index: I) -> Result where T: ValueType, { - T::try_from(self.values.get(col).unwrap().clone()).map_err(|e| DbErr::Type(e.to_string())) + if let Some(index) = index.as_str() { + T::try_from(self.values.get(index).unwrap().clone()) + .map_err(|e| DbErr::Type(e.to_string())) + } else if let Some(index) = index.as_usize() { + let (_, value) = self.values.iter().nth(*index).ok_or_else(|| { + DbErr::Query(RuntimeErr::Internal(format!( + "Column at index {} not found", + index + ))) + })?; + T::try_from(value.clone()).map_err(|e| DbErr::Type(e.to_string())) + } else { + unreachable!("Missing ColIdx implementation for MockRow"); + } } /// An iterator over the keys and values of a mock row @@ -686,10 +699,10 @@ mod tests { ); let mocked_row = row.into_mock_row(); - let a_id = mocked_row.try_get::("A_id"); + let a_id = mocked_row.try_get::("A_id"); assert!(a_id.is_ok()); assert_eq!(1, a_id.unwrap()); - let b_id = mocked_row.try_get::("B_id"); + let b_id = mocked_row.try_get::("B_id"); assert!(b_id.is_ok()); assert_eq!(2, b_id.unwrap()); } diff --git a/src/database/transaction.rs b/src/database/transaction.rs index fa8d57755..549cee3a8 100644 --- a/src/database/transaction.rs +++ b/src/database/transaction.rs @@ -275,7 +275,7 @@ impl DatabaseTransaction { if let Err(sqlx::Error::RowNotFound) = err { Ok(None) } else { - err.map_err(|e| sqlx_error_to_query_err(e)) + err.map_err(sqlx_error_to_query_err) } } } diff --git a/src/entity/active_enum.rs b/src/entity/active_enum.rs index c6a8fefff..229f6197c 100644 --- a/src/entity/active_enum.rs +++ b/src/entity/active_enum.rs @@ -152,8 +152,8 @@ where T: ActiveEnum, T::ValueVec: TryGetable, { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - ::try_get(res, pre, col)? + fn try_get_by(res: &QueryResult, index: I) -> Result { + ::try_get_by(res, index)? .into_iter() .map(|value| T::try_from_value(&value).map_err(TryGetError::DbErr)) .collect() diff --git a/src/executor/query.rs b/src/executor/query.rs index 025674de8..c5e20513a 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -21,10 +21,21 @@ pub(crate) enum QueryResultRow { Mock(crate::MockRow), } -/// Constrain any type trying to get a Row in a database +/// An interface to get a value from the query result pub trait TryGetable: Sized { - /// Ensure the type implements this method - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result; + /// Get a value from the query result with an ColIdx + fn try_get_by(res: &QueryResult, index: I) -> Result; + + /// Get a value from the query result with prefixed column name + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + let index = format!("{}{}", pre, col); + Self::try_get_by(res, index.as_str()) + } + + /// Get a value from the query result based on the order in the select expressions + fn try_get_by_index(res: &QueryResult, index: usize) -> Result { + Self::try_get_by(res, index) + } } /// An error from trying to get a row from a Model @@ -50,7 +61,16 @@ impl From for DbErr { // QueryResult // impl QueryResult { - /// Get a Row from a Column + /// Get a value from the query result with an ColIdx + pub fn try_get_by(&self, index: I) -> Result + where + T: TryGetable, + I: ColIdx, + { + Ok(T::try_get_by(self, index)?) + } + + /// Get a value from the query result with prefixed column name pub fn try_get(&self, pre: &str, col: &str) -> Result where T: TryGetable, @@ -58,13 +78,29 @@ impl QueryResult { Ok(T::try_get(self, pre, col)?) } - /// Perform query operations on multiple Columns + /// Get a value from the query result based on the order in the select expressions + pub fn try_get_by_index(&self, idx: usize) -> Result + where + T: TryGetable, + { + Ok(T::try_get_by_index(self, idx)?) + } + + /// Get a tuple value from the query result with prefixed column name pub fn try_get_many(&self, pre: &str, cols: &[String]) -> Result where T: TryGetableMany, { Ok(T::try_get_many(self, pre, cols)?) } + + /// Get a tuple value from the query result based on the order in the select expressions + pub fn try_get_many_by_index(&self) -> Result + where + T: TryGetableMany, + { + Ok(T::try_get_many_by_index(self)?) + } } #[allow(unused_variables)] @@ -88,8 +124,8 @@ impl fmt::Debug for QueryResultRow { // TryGetable // impl TryGetable for Option { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - match T::try_get(res, pre, col) { + fn try_get_by(res: &QueryResult, index: I) -> Result { + match T::try_get_by(res, index) { Ok(v) => Ok(Some(v)), Err(TryGetError::Null(_)) => Ok(None), Err(e) => Err(e), @@ -97,39 +133,133 @@ impl TryGetable for Option { } } +/// Column Index, used by [`TryGetable`] +pub trait ColIdx: std::fmt::Debug + Copy { + #[cfg(feature = "sqlx-mysql")] + /// Type shenanigans + type SqlxMySqlIndex: sqlx::ColumnIndex; + #[cfg(feature = "sqlx-postgres")] + /// Type shenanigans + type SqlxPostgresIndex: sqlx::ColumnIndex; + #[cfg(feature = "sqlx-sqlite")] + /// Type shenanigans + type SqlxSqliteIndex: sqlx::ColumnIndex; + + #[cfg(feature = "sqlx-mysql")] + /// Basically a no-op; only to satisfy a trait bound + fn as_sqlx_mysql_index(&self) -> Self::SqlxMySqlIndex; + #[cfg(feature = "sqlx-postgres")] + /// Basically a no-op; only to satisfy a trait bound + fn as_sqlx_postgres_index(&self) -> Self::SqlxPostgresIndex; + #[cfg(feature = "sqlx-sqlite")] + /// Basically a no-op; only to satisfy a trait bound + fn as_sqlx_sqlite_index(&self) -> Self::SqlxSqliteIndex; + + /// Self must be `&str`, return `None` otherwise + fn as_str(&self) -> Option<&str>; + /// Self must be `usize`, return `None` otherwise + fn as_usize(&self) -> Option<&usize>; +} + +impl ColIdx for &str { + #[cfg(feature = "sqlx-mysql")] + type SqlxMySqlIndex = Self; + #[cfg(feature = "sqlx-postgres")] + type SqlxPostgresIndex = Self; + #[cfg(feature = "sqlx-sqlite")] + type SqlxSqliteIndex = Self; + + #[cfg(feature = "sqlx-mysql")] + #[inline] + fn as_sqlx_mysql_index(&self) -> Self::SqlxMySqlIndex { + self + } + #[cfg(feature = "sqlx-postgres")] + #[inline] + fn as_sqlx_postgres_index(&self) -> Self::SqlxPostgresIndex { + self + } + #[cfg(feature = "sqlx-sqlite")] + #[inline] + fn as_sqlx_sqlite_index(&self) -> Self::SqlxSqliteIndex { + self + } + + #[inline] + fn as_str(&self) -> Option<&str> { + Some(self) + } + #[inline] + fn as_usize(&self) -> Option<&usize> { + None + } +} + +impl ColIdx for usize { + #[cfg(feature = "sqlx-mysql")] + type SqlxMySqlIndex = Self; + #[cfg(feature = "sqlx-postgres")] + type SqlxPostgresIndex = Self; + #[cfg(feature = "sqlx-sqlite")] + type SqlxSqliteIndex = Self; + + #[cfg(feature = "sqlx-mysql")] + #[inline] + fn as_sqlx_mysql_index(&self) -> Self::SqlxMySqlIndex { + *self + } + #[cfg(feature = "sqlx-postgres")] + #[inline] + fn as_sqlx_postgres_index(&self) -> Self::SqlxPostgresIndex { + *self + } + #[cfg(feature = "sqlx-sqlite")] + #[inline] + fn as_sqlx_sqlite_index(&self) -> Self::SqlxSqliteIndex { + *self + } + + #[inline] + fn as_str(&self) -> Option<&str> { + None + } + #[inline] + fn as_usize(&self) -> Option<&usize> { + Some(self) + } +} + macro_rules! try_getable_all { ( $type: ty ) => { - #[allow(unused_variables)] impl TryGetable for $type { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let column = format!("{}{}", pre, col); + #[allow(unused_variables)] + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "mock")] - #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -142,16 +272,15 @@ macro_rules! try_getable_all { macro_rules! try_getable_unsigned { ( $type: ty ) => { impl TryGetable for $type { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - #[allow(unused_variables)] - let column = format!("{}{}", pre, col); + #[allow(unused_variables)] + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(_) => { @@ -160,15 +289,14 @@ macro_rules! try_getable_unsigned { #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "mock")] - #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -181,16 +309,15 @@ macro_rules! try_getable_unsigned { macro_rules! try_getable_mysql { ( $type: ty ) => { impl TryGetable for $type { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - #[allow(unused_variables)] - let column = format!("{}{}", pre, col); + #[allow(unused_variables)] + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(_) => { @@ -201,10 +328,9 @@ macro_rules! try_getable_mysql { panic!("{} unsupported by sqlx-sqlite", stringify!($type)) } #[cfg(feature = "mock")] - #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -218,40 +344,38 @@ macro_rules! try_getable_mysql { macro_rules! try_getable_date_time { ( $type: ty ) => { impl TryGetable for $type { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - #[allow(unused_variables)] - let column = format!("{}{}", pre, col); + #[allow(unused_variables)] + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use chrono::{DateTime, Utc}; use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) .map(|v| v.into()) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use chrono::{DateTime, Utc}; use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) .map(|v| v.into()) } #[cfg(feature = "mock")] - #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -313,28 +437,27 @@ use rust_decimal::Decimal; #[cfg(feature = "with-rust_decimal")] impl TryGetable for Decimal { #[allow(unused_variables)] - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let column = format!("{}{}", pre, col); + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; let val: Option = row - .try_get(column.as_str()) + .try_get(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?; match val { Some(v) => Decimal::try_from(v).map_err(|e| { @@ -344,14 +467,14 @@ impl TryGetable for Decimal { source: Box::new(e), }) }), - None => Err(TryGetError::Null(column)), + None => Err(err_null_idx_col(idx)), } } #[cfg(feature = "mock")] #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -365,28 +488,27 @@ use bigdecimal::BigDecimal; #[cfg(feature = "with-bigdecimal")] impl TryGetable for BigDecimal { #[allow(unused_variables)] - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let column = format!("{}{}", pre, col); + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; let val: Option = row - .try_get(column.as_str()) + .try_get(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?; match val { Some(v) => BigDecimal::try_from(v).map_err(|e| { @@ -396,14 +518,14 @@ impl TryGetable for BigDecimal { source: Box::new(e), }) }), - None => Err(TryGetError::Null(column)), + None => Err(err_null_idx_col(idx)), } } #[cfg(feature = "mock")] #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -415,16 +537,15 @@ impl TryGetable for BigDecimal { try_getable_all!(uuid::Uuid); impl TryGetable for u32 { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - #[allow(unused_variables)] - let column = format!("{}{}", pre, col); + #[allow(unused_variables)] + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { @@ -432,23 +553,23 @@ impl TryGetable for u32 { // Since 0.6.0, SQLx has dropped direct mapping from PostgreSQL's OID to Rust's `u32`; // Instead, `u32` was wrapped by a `sqlx::Oid`. use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) .map(|oid| oid.0) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; - row.try_get::, _>(column.as_str()) + row.try_get::, _>(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "mock")] #[allow(unused_variables)] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -456,6 +577,11 @@ impl TryGetable for u32 { } } +#[allow(dead_code)] +fn err_null_idx_col(idx: I) -> TryGetError { + TryGetError::Null(format!("{:?}", idx)) +} + #[cfg(feature = "postgres-array")] mod postgres_array { use super::*; @@ -463,30 +589,30 @@ mod postgres_array { #[allow(unused_macros)] macro_rules! try_getable_postgres_array { ( $type: ty ) => { + #[allow(unused_variables)] impl TryGetable for Vec<$type> { - #[allow(unused_variables)] - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let column = format!("{}{}", pre, col); + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] - QueryResultRow::SqlxMySql(row) => { + QueryResultRow::SqlxMySql(_) => { panic!("{} unsupported by sqlx-mysql", stringify!($type)) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(_) => { panic!("{} unsupported by sqlx-sqlite", stringify!($type)) } #[cfg(feature = "mock")] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + #[allow(unused_variables)] + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -549,11 +675,10 @@ mod postgres_array { impl TryGetable for Vec { #[allow(unused_variables)] - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let column = format!("{}{}", pre, col); + fn try_get_by(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] - QueryResultRow::SqlxMySql(row) => { + QueryResultRow::SqlxMySql(_) => { panic!("{} unsupported by sqlx-mysql", stringify!($type)) } #[cfg(feature = "sqlx-postgres")] @@ -562,9 +687,9 @@ mod postgres_array { // Since 0.6.0, SQLx has dropped direct mapping from PostgreSQL's OID to Rust's `u32`; // Instead, `u32` was wrapped by a `sqlx::Oid`. use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column))) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))) .map(|oids| oids.into_iter().map(|oid| oid.0).collect()) } #[cfg(feature = "sqlx-sqlite")] @@ -572,9 +697,10 @@ mod postgres_array { panic!("{} unsupported by sqlx-sqlite", stringify!($type)) } #[cfg(feature = "mock")] - QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| { + #[allow(unused_variables)] + QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }), #[allow(unreachable_patterns)] _ => unreachable!(), @@ -585,11 +711,14 @@ mod postgres_array { // TryGetableMany // -/// Perform a query on multiple columns +/// An interface to get a tuple value from the query result pub trait TryGetableMany: Sized { - /// THe method to perform a query on multiple columns + /// Get a tuple value from the query result with prefixed column name fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result; + /// Get a tuple value from the query result based on the order in the select expressions + fn try_get_many_by_index(res: &QueryResult) -> Result; + /// ``` /// # use sea_orm::{error::*, tests_cfg::*, *}; /// # @@ -663,6 +792,10 @@ where try_get_many_with_slice_len_of(1, cols)?; T::try_get(res, pre, &cols[0]) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + T::try_get_by_index(res, 0) + } } impl TryGetableMany for (T,) @@ -672,6 +805,10 @@ where fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { T::try_get_many(res, pre, cols).map(|r| (r,)) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + T::try_get_many_by_index(res).map(|r| (r,)) + } } impl TryGetableMany for (A, B) @@ -686,6 +823,10 @@ where B::try_get(res, pre, &cols[1])?, )) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + Ok((A::try_get_by_index(res, 0)?, B::try_get_by_index(res, 1)?)) + } } impl TryGetableMany for (A, B, C) @@ -702,6 +843,14 @@ where C::try_get(res, pre, &cols[2])?, )) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + Ok(( + A::try_get_by_index(res, 0)?, + B::try_get_by_index(res, 1)?, + C::try_get_by_index(res, 2)?, + )) + } } impl TryGetableMany for (A, B, C, D) @@ -720,6 +869,15 @@ where D::try_get(res, pre, &cols[3])?, )) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + Ok(( + A::try_get_by_index(res, 0)?, + B::try_get_by_index(res, 1)?, + C::try_get_by_index(res, 2)?, + D::try_get_by_index(res, 3)?, + )) + } } impl TryGetableMany for (A, B, C, D, E) @@ -740,6 +898,16 @@ where E::try_get(res, pre, &cols[4])?, )) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + Ok(( + A::try_get_by_index(res, 0)?, + B::try_get_by_index(res, 1)?, + C::try_get_by_index(res, 2)?, + D::try_get_by_index(res, 3)?, + E::try_get_by_index(res, 4)?, + )) + } } impl TryGetableMany for (A, B, C, D, E, F) @@ -762,6 +930,17 @@ where F::try_get(res, pre, &cols[5])?, )) } + + fn try_get_many_by_index(res: &QueryResult) -> Result { + Ok(( + A::try_get_by_index(res, 0)?, + B::try_get_by_index(res, 1)?, + C::try_get_by_index(res, 2)?, + D::try_get_by_index(res, 3)?, + E::try_get_by_index(res, 4)?, + F::try_get_by_index(res, 5)?, + )) + } } fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> { @@ -778,44 +957,43 @@ fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), Try // TryGetableFromJson // -/// Perform a query on multiple columns +/// An interface to get a JSON from the query result #[cfg(feature = "with-json")] pub trait TryGetableFromJson: Sized where for<'de> Self: serde::Deserialize<'de>, { - /// Ensure the type implements this method + /// Get a JSON from the query result with prefixed column name #[allow(unused_variables, unreachable_code)] - fn try_get_from_json(res: &QueryResult, pre: &str, col: &str) -> Result { - let column = format!("{}{}", pre, col); + fn try_get_from_json(res: &QueryResult, idx: I) -> Result { match &res.row { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_mysql_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column)).map(|json| json.0)) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)) } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(row) => { use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_postgres_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column)).map(|json| json.0)) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; - row.try_get::>, _>(column.as_str()) + row.try_get::>, _>(idx.as_sqlx_sqlite_index()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) - .and_then(|opt| opt.ok_or(TryGetError::Null(column)).map(|json| json.0)) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)) } #[cfg(feature = "mock")] QueryResultRow::Mock(row) => row - .try_get::(column.as_str()) + .try_get::(idx) .map_err(|e| { debug_print!("{:#?}", e.to_string()); - TryGetError::Null(column) + err_null_idx_col(idx) }) .and_then(|json| { serde_json::from_value(json) @@ -832,8 +1010,8 @@ impl TryGetable for T where T: TryGetableFromJson, { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - T::try_get_from_json(res, pre, col) + fn try_get_by(res: &QueryResult, index: I) -> Result { + T::try_get_from_json(res, index) } } diff --git a/src/executor/select.rs b/src/executor/select.rs index a31b2a831..bcb5cb22b 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -41,7 +41,7 @@ pub trait SelectorTrait { fn from_raw_query_result(res: QueryResult) -> Result; } -/// Perform an operation on an entity that can yield a Value +/// Get tuple from query result based on a list of column identifiers #[derive(Debug)] pub struct SelectGetableValue where @@ -52,6 +52,15 @@ where model: PhantomData, } +/// Get tuple from query result based on column index +#[derive(Debug)] +pub struct SelectGetableTuple +where + T: TryGetableMany, +{ + model: PhantomData, +} + /// Defines a type to get a Model #[derive(Debug)] pub struct SelectModel @@ -84,6 +93,17 @@ where } } +impl SelectorTrait for SelectGetableTuple +where + T: TryGetableMany, +{ + type Item = T; + + fn from_raw_query_result(res: QueryResult) -> Result { + T::try_get_many_by_index(&res).map_err(Into::into) + } +} + impl SelectorTrait for SelectModel where M: FromQueryResult + Sized, @@ -253,6 +273,104 @@ where Selector::>::with_columns(self.query) } + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(all(feature = "mock", feature = "macros"))] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![ + /// # maplit::btreemap! { + /// # "cake_name" => Into::::into("Chocolate Forest"), + /// # }, + /// # maplit::btreemap! { + /// # "cake_name" => Into::::into("New York Cheese"), + /// # }, + /// # ]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// let res: Vec = cake::Entity::find() + /// .select_only() + /// .column(cake::Column::Name) + /// .into_tuple() + /// .all(&db) + /// .await?; + /// + /// assert_eq!( + /// res, + /// vec!["Chocolate Forest".to_owned(), "New York Cheese".to_owned()] + /// ); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."name" FROM "cake""#, + /// vec![] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(all(feature = "mock", feature = "macros"))] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![ + /// # maplit::btreemap! { + /// # "cake_name" => Into::::into("Chocolate Forest"), + /// # "num_of_cakes" => Into::::into(2i64), + /// # }, + /// # ]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// let res: Vec<(String, i64)> = cake::Entity::find() + /// .select_only() + /// .column(cake::Column::Name) + /// .column(cake::Column::Id) + /// .group_by(cake::Column::Name) + /// .into_tuple() + /// .all(&db) + /// .await?; + /// + /// assert_eq!(res, vec![("Chocolate Forest".to_owned(), 2i64)]); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// vec![ + /// r#"SELECT "cake"."name", "cake"."id""#, + /// r#"FROM "cake" GROUP BY "cake"."name""#, + /// ] + /// .join(" ") + /// .as_str(), + /// vec![] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` + pub fn into_tuple(self) -> Selector> + where + T: TryGetableMany, + { + Selector::>::into_tuple(self.query) + } + /// Get one Model from the SELECT query pub async fn one<'a, C>(self, db: &C) -> Result, DbErr> where @@ -402,7 +520,7 @@ impl Selector where S: SelectorTrait, { - /// Create `Selector` from Statment and columns. Executing this `Selector` + /// Create `Selector` from Statement and columns. Executing this `Selector` /// will return a type `T` which implement `TryGetableMany`. pub fn with_columns(query: SelectStatement) -> Selector> where @@ -418,6 +536,17 @@ where } } + /// Get tuple from query result based on column index + pub fn into_tuple(query: SelectStatement) -> Selector> + where + T: TryGetableMany, + { + Selector { + query, + selector: SelectGetableTuple { model: PhantomData }, + } + } + fn into_selector_raw(self, db: &C) -> SelectorRaw where C: ConnectionTrait, diff --git a/tests/byte_primary_key_tests.rs b/tests/byte_primary_key_tests.rs index 0ed5e5a61..01b768505 100644 --- a/tests/byte_primary_key_tests.rs +++ b/tests/byte_primary_key_tests.rs @@ -76,5 +76,23 @@ pub async fn create_and_update(db: &DatabaseConnection) -> Result<(), DbErr> { }) ); + assert_eq!( + Entity::find() + .filter(Column::Id.eq(vec![1_u8, 2_u8, 3_u8])) // annotate it as Vec explicitly + .into_values::<_, Column>() + .one(db) + .await?, + Some((vec![1_u8, 2_u8, 3_u8], "First Row (Updated)".to_owned(),)) + ); + + assert_eq!( + Entity::find() + .filter(Column::Id.eq(vec![1_u8, 2_u8, 3_u8])) // annotate it as Vec explicitly + .into_tuple() + .one(db) + .await?, + Some((vec![1_u8, 2_u8, 3_u8], "First Row (Updated)".to_owned(),)) + ); + Ok(()) } diff --git a/tests/common/features/event_trigger.rs b/tests/common/features/event_trigger.rs index cea0a8063..de6895327 100644 --- a/tests/common/features/event_trigger.rs +++ b/tests/common/features/event_trigger.rs @@ -39,8 +39,8 @@ impl From for Value { } impl TryGetable for Events { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let vec: Vec = res.try_get(pre, col).map_err(TryGetError::DbErr)?; + fn try_get_by(res: &QueryResult, idx: I) -> Result { + let vec: Vec = res.try_get_by(idx).map_err(TryGetError::DbErr)?; Ok(Events(vec.into_iter().map(Event).collect())) } } diff --git a/tests/common/features/json_vec.rs b/tests/common/features/json_vec.rs index 8e9aca5bd..8e2fdcc60 100644 --- a/tests/common/features/json_vec.rs +++ b/tests/common/features/json_vec.rs @@ -25,8 +25,8 @@ impl From for Value { } impl TryGetable for StringVec { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - let json_str: String = res.try_get(pre, col).map_err(TryGetError::DbErr)?; + fn try_get_by(res: &QueryResult, idx: I) -> Result { + let json_str: String = res.try_get_by(idx).map_err(TryGetError::DbErr)?; serde_json::from_str(&json_str).map_err(|e| TryGetError::DbErr(DbErr::Json(e.to_string()))) } }