diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index d69048e698..5b9055d9cd 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -50,6 +50,7 @@ features = [ "runtime-tokio", "migrate", "any", + "json" ] [features] diff --git a/sqlx-core/src/any/arguments.rs b/sqlx-core/src/any/arguments.rs index 59d6f4d6e0..31e7a8c1f5 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; @@ -57,6 +57,7 @@ impl AnyArguments { Arc: Type + Encode<'a, A::Database>, Arc: Type + Encode<'a, A::Database>, Arc>: Type + Encode<'a, A::Database>, + A::Database: AnyJson, { let mut out = A::default(); @@ -71,6 +72,11 @@ impl AnyArguments { 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), @@ -80,6 +86,8 @@ impl AnyArguments { AnyValueKind::Text(t) => out.add(t), AnyValueKind::TextSlice(t) => out.add(t), AnyValueKind::Blob(b) => out.add(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 9d37bbf5ab..0a392b6920 100644 --- a/sqlx-core/src/any/mod.rs +++ b/sqlx-core/src/any/mod.rs @@ -47,6 +47,52 @@ use crate::types::Type; #[doc(hidden)] pub use value::AnyValueKind; +/// Encode and decode support for JSON with the `Any` driver. +#[doc(hidden)] +pub trait AnyJson: crate::database::Database { + #[cfg(feature = "json")] + fn add_json( + args: &mut A, + value: Box, + ) -> Result<(), crate::error::BoxDynError> + where + A: crate::arguments::Arguments; + + #[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 {} + +#[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, + { + 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..2ae9efe2ae 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>, @@ -127,6 +128,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..c4ba3a84d4 --- /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 a85c1dc69c..2368d85675 100644 --- a/sqlx-core/src/any/value.rs +++ b/sqlx-core/src/any/value.rs @@ -19,6 +19,9 @@ pub enum AnyValueKind { Text(Arc), TextSlice(Arc), Blob(Arc>), + #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] + Json(Box), } impl AnyValueKind { @@ -35,6 +38,8 @@ impl AnyValueKind { AnyValueKind::Text(_) => AnyTypeInfoKind::Text, AnyValueKind::TextSlice(_) => AnyTypeInfoKind::Text, AnyValueKind::Blob(_) => AnyTypeInfoKind::Blob, + #[cfg(feature = "json")] + AnyValueKind::Json(_) => AnyTypeInfoKind::Json, }, } } diff --git a/sqlx-mysql/src/any.rs b/sqlx-mysql/src/any.rs index 241900560e..d76923762c 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 51eb15d2a7..790f0f5eb0 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 83b141decd..eac8995e91 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 71c561cadb..422e63640a 100644 --- a/tests/any/any.rs +++ b/tests/any/any.rs @@ -5,6 +5,11 @@ use sqlx_core::sql_str::AssertSqlSafe; use sqlx_core::Error; 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(); @@ -203,6 +208,67 @@ async fn it_can_query_by_string_args() -> sqlx::Result<()> { assert_eq!(column_0, string); } + 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(()) }