diff --git a/sqlx-core/src/lib.rs b/sqlx-core/src/lib.rs index 6f2e8913..3fb3d656 100644 --- a/sqlx-core/src/lib.rs +++ b/sqlx-core/src/lib.rs @@ -31,10 +31,11 @@ pub mod encode; mod error; mod execute; mod executor; -mod isolation_level; mod from_row; +mod isolation_level; mod options; mod query_result; +mod raw_value; pub mod row; mod runtime; mod r#type; @@ -64,14 +65,15 @@ pub use connection::Connection; pub use database::Database; pub use decode::Decode; pub use encode::Encode; -pub use from_row::FromRow; pub use error::{DatabaseError, Error, Result}; pub use execute::Execute; pub use executor::Executor; +pub use from_row::FromRow; pub use isolation_level::IsolationLevel; pub use options::ConnectOptions; pub use query_result::QueryResult; -pub use r#type::{Type, TypeDecode, TypeEncode, TypeDecodeOwned}; +pub use r#type::{Type, TypeDecode, TypeDecodeOwned, TypeEncode}; +pub use raw_value::RawValue; pub use row::{ColumnIndex, Row}; #[cfg(feature = "actix")] pub use runtime::Actix; diff --git a/sqlx-core/src/row.rs b/sqlx-core/src/row.rs index a4b6df1b..3a1b7df4 100644 --- a/sqlx-core/src/row.rs +++ b/sqlx-core/src/row.rs @@ -1,5 +1,7 @@ +use std::any; + use crate::database::HasRawValue; -use crate::{Database, Decode}; +use crate::{decode, Database, Error, RawValue, Result, TypeDecode, TypeInfo}; /// A single row from a result set generated from the database. pub trait Row: 'static + Send + Sync { @@ -20,82 +22,182 @@ pub trait Row: 'static + Send + Sync { fn columns(&self) -> &[::Column]; /// Returns the column at the index, if available. - fn column>(&self, index: I) -> &::Column; + fn column>(&self, index: I) -> &::Column { + self.try_column(index).unwrap() + } /// Returns the column at the index, if available. fn try_column>( &self, index: I, - ) -> crate::Result<&::Column>; + ) -> Result<&::Column>; /// Returns the column name, given the index of the column. - fn column_name_of(&self, index: usize) -> &str; - - /// Returns the column name, given the index of the column. - fn try_column_name_of(&self, index: usize) -> crate::Result<&str>; + fn column_name(&self, index: usize) -> Option<&str>; /// Returns the column index, given the name of the column. - fn index_of(&self, name: &str) -> usize; + fn column_index(&self, name: &str) -> Option; - /// Returns the column index, given the name of the column. - fn try_index_of(&self, name: &str) -> crate::Result; - - /// Returns the decoded value at the index. + /// Returns the value for a column. + /// + /// # Panics + /// + /// Will panic for any errors documented in [`try_get`]. + /// fn get<'r, T, I>(&'r self, index: I) -> T where I: ColumnIndex, - T: Decode<'r, Self::Database>; + T: TypeDecode<'r, Self::Database>, + { + self.try_get(index).unwrap() + } - /// Returns the decoded value at the index. - fn try_get<'r, T, I>(&'r self, index: I) -> crate::Result + /// Returns the _unchecked_ value for a column. + /// + /// # Panics + /// + /// Will panic for any errors documented in [`try_get_unchecked`]. + /// + fn get_unchecked<'r, T, I>(&'r self, index: I) -> T where I: ColumnIndex, - T: Decode<'r, Self::Database>; + T: TypeDecode<'r, Self::Database>, + { + self.try_get_unchecked(index).unwrap() + } - /// Returns the raw representation of the value at the index. + /// Returns the value for a column. + /// + /// # Errors + /// + /// - Will return `Error::ColumnNotFound` or `Error::ColumnIndexOutOfBounds` if the + /// there is no column with the given name or index. + /// + /// - Will return `Error::ColumnDecode` if there was an issue decoding + /// the value. Common reasons include: + /// + /// - The SQL value is `NULL` and `T` is not `Option` + /// + /// - The SQL value cannot be represented as a `T`. Truncation or + /// loss of precision are considered errors. + /// + /// - The SQL type is not [`compatible`][Type::compatible] with + /// the Rust type. + /// + /// + fn try_get<'r, T, I>(&'r self, index: I) -> Result + where + I: ColumnIndex, + T: TypeDecode<'r, Self::Database>, + { + let value = self.try_get_raw(&index)?; + + let res = if !T::compatible(value.type_info()) { + Err(decode::Error::TypeNotCompatible { + rust_type_name: any::type_name::(), + sql_type_name: value.type_info().name(), + }) + } else { + T::decode(value) + }; + + res.map_err(|err| Error::column_decode(self.column(&index), err)) + } + + /// Returns the _unchecked_ value for a column. + /// + /// In this case, _unchecked_ does not mean `unsafe`. Unlike [`try_get`], + /// this method will not check that the source SQL value is compatible + /// with the target Rust type. This may result in *weird* behavior (reading + /// a `bool` from bytes of a `TEXT` column) but it is not `unsafe` and + /// cannot cause undefined behavior. + /// + /// The type-compatible checks in SQLx will likely never be perfect. This + /// method exists to work-around them in a controlled scenario. + /// + /// # Errors + /// + /// - Will return `Error::ColumnNotFound` or `Error::ColumnIndexOutOfBounds` if the + /// there is no column with the given name or index. + /// + /// - Will return `Error::ColumnDecode` if there was an issue decoding + /// the value. Common reasons include: + /// + /// - The SQL value is `NULL` and `T` is not `Option` + /// + /// - The SQL value cannot be represented as a `T`. Truncation or + /// loss of precision are considered errors. + /// + fn try_get_unchecked<'r, T, I>(&'r self, index: I) -> Result + where + I: ColumnIndex, + T: TypeDecode<'r, Self::Database>, + { + let value = self.try_get_raw(&index)?; + + T::decode(value).map_err(|err| Error::column_decode(self.column(&index), err)) + } + + /// Returns the raw representation of the value for a column. + /// + /// # Panics + /// + /// Will panic for any errors documented in [`try_get_raw`]. + /// #[allow(clippy::needless_lifetimes)] fn get_raw<'r, I: ColumnIndex>( &'r self, index: I, - ) -> >::RawValue; + ) -> >::RawValue { + self.try_get_raw(index).unwrap() + } - /// Returns the raw representation of the value at the index. + /// Returns the raw representation of the value for a column. + /// + /// # Errors + /// + /// - Will return `Error::ColumnNotFound` or `Error::ColumnIndexOutOfBounds` if the + /// there is no column with the given name or index. + /// #[allow(clippy::needless_lifetimes)] fn try_get_raw<'r, I: ColumnIndex>( &'r self, index: I, - ) -> crate::Result<>::RawValue>; + ) -> Result<>::RawValue>; } /// A helper trait used for indexing into a [`Row`]. pub trait ColumnIndex { /// Returns the index of the column at this index, if present. #[allow(clippy::needless_lifetimes)] - fn get<'r>(&self, row: &'r R) -> crate::Result; + fn get<'r>(&self, row: &'r R) -> Result; } // access by index -impl ColumnIndex for usize { +impl ColumnIndex for usize { #[allow(clippy::needless_lifetimes)] - fn get<'r>(&self, _row: &'r R) -> crate::Result { - // note: the "index out of bounds" error will be surfaced - // by [try_get] + fn get<'r>(&self, row: &'r R) -> Result { + if *self >= row.len() { + return Err(Error::ColumnIndexOutOfBounds { len: row.len(), index: *self }); + } + Ok(*self) } } // access by name -impl ColumnIndex for &'_ str { +impl ColumnIndex for &'_ str { #[allow(clippy::needless_lifetimes)] - fn get<'r>(&self, row: &'r R) -> crate::Result { - row.try_index_of(self) + fn get<'r>(&self, row: &'r R) -> Result { + row.column_index(self) + .ok_or_else(|| Error::ColumnNotFound { name: self.to_string().into_boxed_str() }) } } // access by reference -impl> ColumnIndex for &'_ I { +impl> ColumnIndex for &'_ I { #[allow(clippy::needless_lifetimes)] - fn get<'r>(&self, row: &'r R) -> crate::Result { + fn get<'r>(&self, row: &'r R) -> Result { (*self).get(row) } }