Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Text adapter #2894

Merged
merged 1 commit into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,3 @@ required-features = ["postgres", "macros", "migrate"]
name = "postgres-migrate"
path = "tests/postgres/migrate.rs"
required-features = ["postgres", "macros", "migrate"]

2 changes: 1 addition & 1 deletion sqlx-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,5 @@ event-listener = "2.5.2"
dotenvy = "0.15"

[dev-dependencies]
sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros"] }
sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] }
tokio = { version = "1", features = ["rt"] }
4 changes: 4 additions & 0 deletions sqlx-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub mod bstr;
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
mod json;

mod text;

#[cfg(feature = "uuid")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
#[doc(no_inline)]
Expand Down Expand Up @@ -81,6 +83,8 @@ pub mod mac_address {
#[cfg(feature = "json")]
pub use json::{Json, JsonRawValue, JsonValue};

pub use text::Text;

/// Indicates that a SQL type is supported for a database.
///
/// ## Compile-time verification
Expand Down
134 changes: 134 additions & 0 deletions sqlx-core/src/types/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::ops::{Deref, DerefMut};

/// Map a SQL text value to/from a Rust type using [`Display`] and [`FromStr`].
///
/// This can be useful for types that do not have a direct SQL equivalent, or are simply not
/// supported by SQLx for one reason or another.
///
/// For strongly typed databases like Postgres, this will report the value's type as `TEXT`.
/// Explicit conversion may be necessary on the SQL side depending on the desired type.
///
/// [`Display`]: std::fmt::Display
/// [`FromStr`]: std::str::FromStr
///
/// ### Panics
///
/// You should only use this adapter with `Display` implementations that are infallible,
/// otherwise you may encounter panics when attempting to bind a value.
///
/// This is because the design of the `Encode` trait assumes encoding is infallible, so there is no
/// way to bubble up the error.
///
/// Fortunately, most `Display` implementations are infallible by convention anyway
/// (the standard `ToString` trait also assumes this), but you may still want to audit
/// the source code for any types you intend to use with this adapter, just to be safe.
///
/// ### Example: `SocketAddr`
///
/// MySQL and SQLite do not have a native SQL equivalent for `SocketAddr`, so if you want to
/// store and retrieve instances of it, it makes sense to map it to `TEXT`:
///
/// ```rust,no_run
/// # use sqlx::types::{time, uuid};
///
/// use std::net::SocketAddr;
///
/// use sqlx::Connection;
/// use sqlx::mysql::MySqlConnection;
/// use sqlx::types::Text;
///
/// use uuid::Uuid;
/// use time::OffsetDateTime;
///
/// #[derive(sqlx::FromRow, Debug)]
/// struct Login {
/// user_id: Uuid,
/// socket_addr: Text<SocketAddr>,
/// login_at: OffsetDateTime
/// }
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
///
/// let mut conn: MySqlConnection = MySqlConnection::connect("<DATABASE URL>").await?;
///
/// let user_id: Uuid = "e9a72cdc-d907-48d6-a488-c64a91fd063c".parse().unwrap();
/// let socket_addr: SocketAddr = "198.51.100.47:31790".parse().unwrap();
///
/// // CREATE TABLE user_login(user_id VARCHAR(36), socket_addr TEXT, login_at TIMESTAMP);
/// sqlx::query("INSERT INTO user_login(user_id, socket_addr, login_at) VALUES (?, ?, NOW())")
/// .bind(user_id)
/// .bind(Text(socket_addr))
/// .execute(&mut conn)
/// .await?;
///
/// let logins: Vec<Login> = sqlx::query_as("SELECT * FROM user_login")
/// .fetch_all(&mut conn)
/// .await?;
///
/// println!("Logins for user ID {user_id}: {logins:?}");
///
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Text<T>(pub T);

impl<T> Text<T> {
/// Extract the inner value.
pub fn into_inner(self) -> T {
self.0
}
}

impl<T> Deref for Text<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> DerefMut for Text<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

/* We shouldn't use blanket impls so individual drivers can provide specialized ones.
impl<T, DB> Type<DB> for Text<T>
where
String: Type<DB>,
DB: Database,
{
fn type_info() -> DB::TypeInfo {
String::type_info()
}

fn compatible(ty: &DB::TypeInfo) -> bool {
String::compatible(ty)
}
}

impl<'q, T, DB> Encode<'q, DB> for Text<T>
where
T: Display,
String: Encode<'q, DB>,
DB: Database,
{
fn encode_by_ref(&self, buf: &mut <DB as HasArguments<'q>>::ArgumentBuffer) -> IsNull {
self.0.to_string().encode(buf)
}
}

impl<'r, T, DB> Decode<'r, DB> for Text<T>
where
T: FromStr,
BoxDynError: From<<T as FromStr>::Err>,
&'r str: Decode<'r, DB>,
DB: Database,
{
fn decode(value: <DB as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
Ok(Text(<&'r str as Decode<'r, DB>>::decode(value)?.parse()?))
}
}
*/
1 change: 1 addition & 0 deletions sqlx-mysql/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ mod bytes;
mod float;
mod int;
mod str;
mod text;
mod uint;

#[cfg(feature = "json")]
Expand Down
49 changes: 49 additions & 0 deletions sqlx-mysql/src/types/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
use sqlx_core::decode::Decode;
use sqlx_core::encode::{Encode, IsNull};
use sqlx_core::error::BoxDynError;
use sqlx_core::types::{Text, Type};
use std::fmt::Display;
use std::str::FromStr;

impl<T> Type<MySql> for Text<T> {
fn type_info() -> MySqlTypeInfo {
<String as Type<MySql>>::type_info()
}

fn compatible(ty: &MySqlTypeInfo) -> bool {
<String as Type<MySql>>::compatible(ty)
}
}

impl<'q, T> Encode<'q, MySql> for Text<T>
where
T: Display,
{
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
// We can't really do the trick like with Postgres where we reserve the space for the
// length up-front and then overwrite it later, because MySQL appears to enforce that
// length-encoded integers use the smallest encoding for the value:
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html#sect_protocol_basic_dt_int_le
//
// So we'd have to reserve space for the max-width encoding, format into the buffer,
// then figure out how many bytes our length-encoded integer needs to be and move the
// value bytes down to use up the empty space.
//
// Copying from a completely separate buffer instead is easier. It may or may not be faster
// or slower depending on a ton of different variables, but I don't currently have the time
// to implement both approaches and compare their performance.
Encode::<MySql>::encode(self.0.to_string(), buf)
}
}

impl<'r, T> Decode<'r, MySql> for Text<T>
where
T: FromStr,
BoxDynError: From<<T as FromStr>::Err>,
{
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
let s: &str = Decode::<MySql>::decode(value)?;
Ok(Self(s.parse()?))
}
}
11 changes: 11 additions & 0 deletions sqlx-postgres/src/types/array.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use sqlx_core::bytes::Buf;
use sqlx_core::types::Text;
use std::borrow::Cow;

use crate::decode::Decode;
Expand Down Expand Up @@ -67,6 +68,16 @@ where
}
}

impl<T> PgHasArrayType for Text<T> {
fn array_type_info() -> PgTypeInfo {
String::array_type_info()
}

fn array_compatible(ty: &PgTypeInfo) -> bool {
String::array_compatible(ty)
}
}

impl<T> Type<Postgres> for [T]
where
T: PgHasArrayType,
Expand Down
1 change: 1 addition & 0 deletions sqlx-postgres/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ mod oid;
mod range;
mod record;
mod str;
mod text;
mod tuple;
mod void;

Expand Down
50 changes: 50 additions & 0 deletions sqlx-postgres/src/types/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres};
use sqlx_core::decode::Decode;
use sqlx_core::encode::{Encode, IsNull};
use sqlx_core::error::BoxDynError;
use sqlx_core::types::{Text, Type};
use std::fmt::Display;
use std::str::FromStr;

use std::io::Write;

impl<T> Type<Postgres> for Text<T> {
fn type_info() -> PgTypeInfo {
<String as Type<Postgres>>::type_info()
}

fn compatible(ty: &PgTypeInfo) -> bool {
<String as Type<Postgres>>::compatible(ty)
}
}

impl<'q, T> Encode<'q, Postgres> for Text<T>
where
T: Display,
{
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
// Unfortunately, our API design doesn't give us a way to bubble up the error here.
//
// Fortunately, writing to `Vec<u8>` is infallible so the only possible source of
// errors is from the implementation of `Display::fmt()` itself,
// where the onus is on the user.
//
// The blanket impl of `ToString` also panics if there's an error, so this is not
// unprecedented.
//
// However, the panic should be documented anyway.
write!(**buf, "{}", self.0).expect("unexpected error from `Display::fmt()`");
IsNull::No
}
}

impl<'r, T> Decode<'r, Postgres> for Text<T>
where
T: FromStr,
BoxDynError: From<<T as FromStr>::Err>,
{
fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
let s: &str = Decode::<Postgres>::decode(value)?;
Ok(Self(s.parse()?))
}
}
7 changes: 5 additions & 2 deletions sqlx-sqlite/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@
//! over a floating-point type in the first place.
//!
//! Instead, you should only use a type affinity that SQLite will not attempt to convert implicitly,
//! such as `TEXT` or `BLOB`, and map values to/from SQLite as strings.
//! such as `TEXT` or `BLOB`, and map values to/from SQLite as strings. You can do this easily
//! using [the `Text` adapter].
//!
//!
//! [`decimal.c`]: https://www.sqlite.org/floatingpoint.html#the_decimal_c_extension
//! [amalgamation]: https://www.sqlite.org/amalgamation.html
//! [type-affinity]: https://www.sqlite.org/datatype3.html#type_affinity
//!
//! [the `Text` adapter]: Text

pub(crate) use sqlx_core::types::*;

Expand All @@ -142,6 +144,7 @@ mod int;
#[cfg(feature = "json")]
mod json;
mod str;
mod text;
#[cfg(feature = "time")]
mod time;
mod uint;
Expand Down
37 changes: 37 additions & 0 deletions sqlx-sqlite/src/types/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::{Sqlite, SqliteArgumentValue, SqliteTypeInfo, SqliteValueRef};
use sqlx_core::decode::Decode;
use sqlx_core::encode::{Encode, IsNull};
use sqlx_core::error::BoxDynError;
use sqlx_core::types::{Text, Type};
use std::fmt::Display;
use std::str::FromStr;

impl<T> Type<Sqlite> for Text<T> {
fn type_info() -> SqliteTypeInfo {
<String as Type<Sqlite>>::type_info()
}

fn compatible(ty: &SqliteTypeInfo) -> bool {
<String as Type<Sqlite>>::compatible(ty)
}
}

impl<'q, T> Encode<'q, Sqlite> for Text<T>
where
T: Display,
{
fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
Encode::<Sqlite>::encode(self.0.to_string(), buf)
}
}

impl<'r, T> Decode<'r, Sqlite> for Text<T>
where
T: FromStr,
BoxDynError: From<<T as FromStr>::Err>,
{
fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
let s: &str = Decode::<Sqlite>::decode(value)?;
Ok(Self(s.parse()?))
}
}
Loading
Loading