diff --git a/sqlx-core/src/any/arguments.rs b/sqlx-core/src/any/arguments.rs index 5a617c1419..938943a0ea 100644 --- a/sqlx-core/src/any/arguments.rs +++ b/sqlx-core/src/any/arguments.rs @@ -1,5 +1,5 @@ use crate::any::value::AnyValueKind; -use crate::any::{Any, AnyTypeInfoKind}; +use crate::any::{Any, AnyJson, AnyTypeInfoKind}; use crate::arguments::Arguments; use crate::encode::{Encode, IsNull}; use crate::error::BoxDynError; @@ -62,6 +62,7 @@ impl<'q> AnyArguments<'q> { f64: Type + Encode<'a, A::Database>, String: Type + Encode<'a, A::Database>, Vec: Type + Encode<'a, A::Database>, + A::Database: AnyJson, { let mut out = A::default(); @@ -76,6 +77,11 @@ impl<'q> AnyArguments<'q> { AnyValueKind::Null(AnyTypeInfoKind::Double) => out.add(Option::::None), AnyValueKind::Null(AnyTypeInfoKind::Text) => out.add(Option::::None), AnyValueKind::Null(AnyTypeInfoKind::Blob) => out.add(Option::>::None), + #[cfg(feature = "json")] + AnyValueKind::Null(AnyTypeInfoKind::Json) => { + let null_json = serde_json::value::RawValue::from_string("null".to_string())?; + A::Database::add_json(&mut out, null_json) + } AnyValueKind::Bool(b) => out.add(b), AnyValueKind::SmallInt(i) => out.add(i), AnyValueKind::Integer(i) => out.add(i), @@ -84,6 +90,8 @@ impl<'q> AnyArguments<'q> { AnyValueKind::Double(d) => out.add(d), AnyValueKind::Text(t) => out.add(String::from(t)), AnyValueKind::Blob(b) => out.add(Vec::from(b)), + #[cfg(feature = "json")] + AnyValueKind::Json(j) => A::Database::add_json(&mut out, j), }? } Ok(out) diff --git a/sqlx-core/src/any/mod.rs b/sqlx-core/src/any/mod.rs index 032f4dda03..1de3b55505 100644 --- a/sqlx-core/src/any/mod.rs +++ b/sqlx-core/src/any/mod.rs @@ -47,6 +47,56 @@ use crate::types::Type; #[doc(hidden)] pub use value::AnyValueKind; +/// Encode and decode support for JSON with the `Any` driver. +/// +/// Exists because `where` bounds on `AnyArguments::convert_into` and `AnyValue::map_from` cannot be +/// conditional on the `json` feature. Notice how this trait is always enabled, but its method aren't. +#[doc(hidden)] +pub trait AnyJson: crate::database::Database { + #[cfg(feature = "json")] + fn add_json<'a, A>( + args: &mut A, + value: Box, + ) -> Result<(), crate::error::BoxDynError> + where + A: crate::arguments::Arguments<'a, Database = Self>; + + #[cfg(feature = "json")] + fn decode_json( + value: ::ValueRef<'_>, + ) -> Result, crate::error::BoxDynError>; +} + +/// No-op impl when `json` feature is disabled. +#[cfg(not(feature = "json"))] +impl AnyJson for DB {} + +/// Full-featured impl +#[cfg(feature = "json")] +impl AnyJson for DB +where + DB: crate::database::Database, + crate::types::Json>: + Type + for<'a> crate::decode::Decode<'a, DB> + for<'a> crate::encode::Encode<'a, DB>, +{ + fn add_json<'a, A>( + args: &mut A, + value: Box, + ) -> Result<(), crate::error::BoxDynError> + where + A: crate::arguments::Arguments<'a, Database = Self>, + { + args.add(crate::types::Json(value)) + } + + fn decode_json( + value: ::ValueRef<'_>, + ) -> Result, crate::error::BoxDynError> { + use crate::decode::Decode; + >>::decode(value).map(|j| j.0) + } +} + pub type AnyPool = crate::pool::Pool; pub type AnyPoolOptions = crate::pool::PoolOptions; diff --git a/sqlx-core/src/any/row.rs b/sqlx-core/src/any/row.rs index 57b8590b5f..dc5319f1b3 100644 --- a/sqlx-core/src/any/row.rs +++ b/sqlx-core/src/any/row.rs @@ -1,5 +1,5 @@ use crate::any::error::mismatched_types; -use crate::any::{Any, AnyColumn, AnyTypeInfo, AnyTypeInfoKind, AnyValue, AnyValueKind}; +use crate::any::{Any, AnyColumn, AnyJson, AnyTypeInfo, AnyTypeInfoKind, AnyValue, AnyValueKind}; use crate::column::{Column, ColumnIndex}; use crate::database::Database; use crate::decode::Decode; @@ -85,6 +85,7 @@ impl AnyRow { ) -> Result where usize: ColumnIndex, + R::Database: AnyJson, AnyTypeInfo: for<'b> TryFrom<&'b ::TypeInfo, Error = Error>, AnyColumn: for<'b> TryFrom<&'b ::Column, Error = Error>, bool: Type + Decode<'a, R::Database>, @@ -95,6 +96,7 @@ impl AnyRow { f64: Type + Decode<'a, R::Database>, String: Type + Decode<'a, R::Database>, Vec: Type + Decode<'a, R::Database>, + R::Database: AnyJson, { let mut row_out = AnyRow { column_names, @@ -127,6 +129,15 @@ impl AnyRow { AnyTypeInfoKind::Double => AnyValueKind::Double(decode(value)?), AnyTypeInfoKind::Blob => AnyValueKind::Blob(decode::<_, Vec>(value)?.into()), AnyTypeInfoKind::Text => AnyValueKind::Text(decode::<_, String>(value)?.into()), + #[cfg(feature = "json")] + AnyTypeInfoKind::Json => { + AnyValueKind::Json(R::Database::decode_json(value).map_err(|e| { + Error::ColumnDecode { + index: col.ordinal().to_string(), + source: e, + } + })?) + } }; row_out.columns.push(any_col); diff --git a/sqlx-core/src/any/type_info.rs b/sqlx-core/src/any/type_info.rs index 0879b333ca..3f4f729c5b 100644 --- a/sqlx-core/src/any/type_info.rs +++ b/sqlx-core/src/any/type_info.rs @@ -27,6 +27,9 @@ pub enum AnyTypeInfoKind { Double, Text, Blob, + #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] + Json, } impl TypeInfo for AnyTypeInfo { @@ -47,6 +50,8 @@ impl TypeInfo for AnyTypeInfo { Text => "TEXT", Blob => "BLOB", Null => "NULL", + #[cfg(feature = "json")] + Json => "JSON", } } } diff --git a/sqlx-core/src/any/types/json.rs b/sqlx-core/src/any/types/json.rs new file mode 100644 index 0000000000..13a22647ab --- /dev/null +++ b/sqlx-core/src/any/types/json.rs @@ -0,0 +1,48 @@ +use crate::any::{Any, AnyArgumentBuffer, AnyTypeInfo, AnyTypeInfoKind, AnyValueKind, AnyValueRef}; +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{Json, Type}; +use serde::{Deserialize, Serialize}; + +impl Type for Json { + fn type_info() -> AnyTypeInfo { + AnyTypeInfo { + kind: AnyTypeInfoKind::Json, + } + } + + fn compatible(ty: &AnyTypeInfo) -> bool { + matches!( + ty.kind, + AnyTypeInfoKind::Json | AnyTypeInfoKind::Text | AnyTypeInfoKind::Blob + ) + } +} + +impl Encode<'_, Any> for Json +where + T: Serialize, +{ + fn encode_by_ref(&self, buf: &mut AnyArgumentBuffer<'_>) -> Result { + let json_string = self.encode_to_string()?; + let raw_value = serde_json::value::RawValue::from_string(json_string)?; + buf.0.push(AnyValueKind::Json(raw_value)); + Ok(IsNull::No) + } +} + +impl Decode<'_, Any> for Json +where + T: for<'de> Deserialize<'de>, +{ + fn decode(value: AnyValueRef<'_>) -> Result { + match value.kind { + #[cfg(feature = "json")] + AnyValueKind::Json(raw) => Json::decode_from_string(raw.get()), + AnyValueKind::Text(text) => Json::decode_from_string(&text), + AnyValueKind::Blob(blob) => Json::decode_from_bytes(&blob), + other => other.unexpected(), + } + } +} diff --git a/sqlx-core/src/any/types/mod.rs b/sqlx-core/src/any/types/mod.rs index a0ae55156d..8a745b765c 100644 --- a/sqlx-core/src/any/types/mod.rs +++ b/sqlx-core/src/any/types/mod.rs @@ -21,6 +21,9 @@ mod blob; mod bool; mod float; mod int; +#[cfg(feature = "json")] +#[cfg_attr(docsrs, doc(cfg(feature = "json")))] +mod json; mod str; #[test] @@ -50,4 +53,8 @@ fn test_type_impls() { // These imply that there are also impls for the equivalent slice types. has_type::>(); has_type::(); + + // JSON types + #[cfg(feature = "json")] + has_type::>(); } diff --git a/sqlx-core/src/any/value.rs b/sqlx-core/src/any/value.rs index 9917b39f4f..766be7299a 100644 --- a/sqlx-core/src/any/value.rs +++ b/sqlx-core/src/any/value.rs @@ -18,6 +18,9 @@ pub enum AnyValueKind<'a> { Double(f64), Text(Cow<'a, str>), Blob(Cow<'a, [u8]>), + #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] + Json(Box), } impl AnyValueKind<'_> { @@ -33,6 +36,8 @@ impl AnyValueKind<'_> { AnyValueKind::Double(_) => AnyTypeInfoKind::Double, AnyValueKind::Text(_) => AnyTypeInfoKind::Text, AnyValueKind::Blob(_) => AnyTypeInfoKind::Blob, + #[cfg(feature = "json")] + AnyValueKind::Json(_) => AnyTypeInfoKind::Json, }, } } @@ -83,6 +88,8 @@ impl Value for AnyValue { AnyValueKind::Double(d) => AnyValueKind::Double(*d), AnyValueKind::Text(t) => AnyValueKind::Text(Cow::Borrowed(t)), AnyValueKind::Blob(b) => AnyValueKind::Blob(Cow::Borrowed(b)), + #[cfg(feature = "json")] + AnyValueKind::Json(j) => AnyValueKind::Json(j.clone()), }, } } @@ -111,6 +118,8 @@ impl<'a> ValueRef<'a> for AnyValueRef<'a> { AnyValueKind::Double(d) => AnyValueKind::Double(*d), AnyValueKind::Text(t) => AnyValueKind::Text(Cow::Owned(t.to_string())), AnyValueKind::Blob(b) => AnyValueKind::Blob(Cow::Owned(b.to_vec())), + #[cfg(feature = "json")] + AnyValueKind::Json(j) => AnyValueKind::Json(j.clone()), }, } } diff --git a/sqlx-core/src/types/json.rs b/sqlx-core/src/types/json.rs index f9d95c9b01..2cff5e149b 100644 --- a/sqlx-core/src/types/json.rs +++ b/sqlx-core/src/types/json.rs @@ -196,20 +196,6 @@ where } } -impl Type for Box -where - for<'a> Json<&'a Self>: Type, - DB: Database, -{ - fn type_info() -> DB::TypeInfo { - as Type>::type_info() - } - - fn compatible(ty: &DB::TypeInfo) -> bool { - as Type>::compatible(ty) - } -} - impl<'q, DB> Encode<'q, DB> for JsonRawValue where for<'a> Json<&'a Self>: Encode<'q, DB>, diff --git a/sqlx-mysql/src/any.rs b/sqlx-mysql/src/any.rs index 70b7ad4511..edeaab13d2 100644 --- a/sqlx-mysql/src/any.rs +++ b/sqlx-mysql/src/any.rs @@ -168,6 +168,8 @@ impl<'a> TryFrom<&'a MySqlTypeInfo> for AnyTypeInfo { ColumnType::String | ColumnType::VarString | ColumnType::VarChar => { AnyTypeInfoKind::Text } + #[cfg(feature = "json")] + ColumnType::Json => AnyTypeInfoKind::Json, _ => { return Err(sqlx_core::Error::AnyDriverError( format!("Any driver does not support MySql type {type_info:?}").into(), diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index d24145637c..dcfaf4ffbd 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -197,6 +197,8 @@ impl<'a> TryFrom<&'a PgTypeInfo> for AnyTypeInfo { PgType::Bytea => AnyTypeInfoKind::Blob, PgType::Text | PgType::Varchar => AnyTypeInfoKind::Text, PgType::DeclareWithName(UStr::Static("citext")) => AnyTypeInfoKind::Text, + #[cfg(feature = "json")] + PgType::Json | PgType::Jsonb => AnyTypeInfoKind::Json, _ => { return Err(sqlx_core::Error::AnyDriverError( format!("Any driver does not support the Postgres type {pg_type:?}").into(), diff --git a/sqlx-sqlite/src/any.rs b/sqlx-sqlite/src/any.rs index 636f986bf5..9a7f2cd738 100644 --- a/sqlx-sqlite/src/any.rs +++ b/sqlx-sqlite/src/any.rs @@ -221,6 +221,8 @@ fn map_arguments(args: AnyArguments<'_>) -> SqliteArguments { AnyValueKind::Double(d) => SqliteArgumentValue::Double(d), AnyValueKind::Text(t) => SqliteArgumentValue::Text(Arc::new(t.to_string())), AnyValueKind::Blob(b) => SqliteArgumentValue::Blob(Arc::new(b.to_vec())), + #[cfg(feature = "json")] + AnyValueKind::Json(j) => SqliteArgumentValue::Text(Arc::new(j.get().to_string())), // AnyValueKind is `#[non_exhaustive]` but we should have covered everything _ => unreachable!("BUG: missing mapping for {val:?}"), }) diff --git a/tests/any/any.rs b/tests/any/any.rs index 62dc20403e..531054d82d 100644 --- a/tests/any/any.rs +++ b/tests/any/any.rs @@ -3,6 +3,11 @@ use sqlx::{Any, Connection, Executor, Row}; use sqlx_core::sql_str::AssertSqlSafe; use sqlx_test::new; +#[cfg(feature = "json")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "json")] +use sqlx::types::Json; + #[sqlx_macros::test] async fn it_connects() -> anyhow::Result<()> { sqlx::any::install_default_drivers(); @@ -142,3 +147,65 @@ async fn it_can_fail_and_recover_with_pool() -> anyhow::Result<()> { Ok(()) } + +#[cfg(feature = "json")] +#[sqlx_macros::test] +async fn it_encodes_decodes_json() -> anyhow::Result<()> { + sqlx::any::install_default_drivers(); + + // Create new connection + let mut conn = new::().await?; + + // Test with serde_json::Value + let json_value = serde_json::json!({ + "name": "test", + "value": 42, + "items": [1, 2, 3] + }); + + // Create temp table: + sqlx::query("create temporary table json_test (data TEXT)") + .execute(&mut conn) + .await?; + + #[cfg(feature = "postgres")] + let query = "insert into json_test (data) values ($1)"; + + #[cfg(not(feature = "postgres"))] + let query = "insert into json_test (data) values (?)"; + + // Insert into the temporary table: + sqlx::query(query) + .bind(Json(&json_value)) + .execute(&mut conn) + .await?; + + // This will work by encoding JSON as text and decoding it back + let result: serde_json::Value = sqlx::query_scalar("select data from json_test") + .fetch_one(&mut conn) + .await?; + + assert_eq!(result, json_value); + + // Test with custom struct + #[derive(Serialize, Deserialize, Debug, PartialEq)] + struct TestData { + name: String, + value: i32, + items: [i32; 3], + } + + let test_data = TestData { + name: "test".to_string(), + value: 42, + items: [1, 2, 3], + }; + + let result: Json = sqlx::query_scalar("select data from json_test") + .fetch_one(&mut conn) + .await?; + + assert_eq!(result.0, test_data); + + Ok(()) +}