Skip to content
Open
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
10 changes: 9 additions & 1 deletion sqlx-core/src/any/arguments.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -62,6 +62,7 @@ impl<'q> AnyArguments<'q> {
f64: Type<A::Database> + Encode<'a, A::Database>,
String: Type<A::Database> + Encode<'a, A::Database>,
Vec<u8>: Type<A::Database> + Encode<'a, A::Database>,
A::Database: AnyJson,
{
let mut out = A::default();

Expand All @@ -76,6 +77,11 @@ impl<'q> AnyArguments<'q> {
AnyValueKind::Null(AnyTypeInfoKind::Double) => out.add(Option::<f32>::None),
AnyValueKind::Null(AnyTypeInfoKind::Text) => out.add(Option::<String>::None),
AnyValueKind::Null(AnyTypeInfoKind::Blob) => out.add(Option::<Vec<u8>>::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),
Expand All @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions sqlx-core/src/any/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::value::RawValue>,
) -> Result<(), crate::error::BoxDynError>
where
A: crate::arguments::Arguments<'a, Database = Self>;

#[cfg(feature = "json")]
fn decode_json(
value: <Self as crate::database::Database>::ValueRef<'_>,
) -> Result<Box<serde_json::value::RawValue>, crate::error::BoxDynError>;
}

/// No-op impl when `json` feature is disabled.
#[cfg(not(feature = "json"))]
impl<DB: crate::database::Database> AnyJson for DB {}

/// Full-featured impl
#[cfg(feature = "json")]
impl<DB> AnyJson for DB
where
DB: crate::database::Database,
crate::types::Json<Box<serde_json::value::RawValue>>:
Type<DB> + for<'a> crate::decode::Decode<'a, DB> + for<'a> crate::encode::Encode<'a, DB>,
{
fn add_json<'a, A>(
args: &mut A,
value: Box<serde_json::value::RawValue>,
) -> Result<(), crate::error::BoxDynError>
where
A: crate::arguments::Arguments<'a, Database = Self>,
{
args.add(crate::types::Json(value))
}

fn decode_json(
value: <Self as crate::database::Database>::ValueRef<'_>,
) -> Result<Box<serde_json::value::RawValue>, crate::error::BoxDynError> {
use crate::decode::Decode;
<crate::types::Json<Box<serde_json::value::RawValue>>>::decode(value).map(|j| j.0)
}
}

pub type AnyPool = crate::pool::Pool<Any>;

pub type AnyPoolOptions = crate::pool::PoolOptions<Any>;
Expand Down
13 changes: 12 additions & 1 deletion sqlx-core/src/any/row.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -85,6 +85,7 @@ impl AnyRow {
) -> Result<Self, Error>
where
usize: ColumnIndex<R>,
R::Database: AnyJson,
AnyTypeInfo: for<'b> TryFrom<&'b <R::Database as Database>::TypeInfo, Error = Error>,
AnyColumn: for<'b> TryFrom<&'b <R::Database as Database>::Column, Error = Error>,
bool: Type<R::Database> + Decode<'a, R::Database>,
Expand All @@ -95,6 +96,7 @@ impl AnyRow {
f64: Type<R::Database> + Decode<'a, R::Database>,
String: Type<R::Database> + Decode<'a, R::Database>,
Vec<u8>: Type<R::Database> + Decode<'a, R::Database>,
R::Database: AnyJson,
{
let mut row_out = AnyRow {
column_names,
Expand Down Expand Up @@ -127,6 +129,15 @@ impl AnyRow {
AnyTypeInfoKind::Double => AnyValueKind::Double(decode(value)?),
AnyTypeInfoKind::Blob => AnyValueKind::Blob(decode::<_, Vec<u8>>(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);
Expand Down
5 changes: 5 additions & 0 deletions sqlx-core/src/any/type_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -47,6 +50,8 @@ impl TypeInfo for AnyTypeInfo {
Text => "TEXT",
Blob => "BLOB",
Null => "NULL",
#[cfg(feature = "json")]
Json => "JSON",
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions sqlx-core/src/any/types/json.rs
Original file line number Diff line number Diff line change
@@ -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<T> Type<Any> for Json<T> {
fn type_info() -> AnyTypeInfo {
AnyTypeInfo {
kind: AnyTypeInfoKind::Json,
}
}

fn compatible(ty: &AnyTypeInfo) -> bool {
matches!(
ty.kind,
AnyTypeInfoKind::Json | AnyTypeInfoKind::Text | AnyTypeInfoKind::Blob
)
}
}

impl<T> Encode<'_, Any> for Json<T>
where
T: Serialize,
{
fn encode_by_ref(&self, buf: &mut AnyArgumentBuffer<'_>) -> Result<IsNull, BoxDynError> {
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<T> Decode<'_, Any> for Json<T>
where
T: for<'de> Deserialize<'de>,
{
fn decode(value: AnyValueRef<'_>) -> Result<Self, BoxDynError> {
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(),
}
}
}
7 changes: 7 additions & 0 deletions sqlx-core/src/any/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -50,4 +53,8 @@ fn test_type_impls() {
// These imply that there are also impls for the equivalent slice types.
has_type::<Vec<u8>>();
has_type::<String>();

// JSON types
#[cfg(feature = "json")]
has_type::<crate::types::Json<serde_json::Value>>();
}
9 changes: 9 additions & 0 deletions sqlx-core/src/any/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::value::RawValue>),
}

impl AnyValueKind<'_> {
Expand All @@ -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,
},
}
}
Expand Down Expand Up @@ -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()),
},
}
}
Expand Down Expand Up @@ -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()),
},
}
}
Expand Down
14 changes: 0 additions & 14 deletions sqlx-core/src/types/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,6 @@ where
}
}

impl<DB> Type<DB> for Box<JsonRawValue>
where
for<'a> Json<&'a Self>: Type<DB>,
DB: Database,
{
fn type_info() -> DB::TypeInfo {
<Json<&Self> as Type<DB>>::type_info()
}

fn compatible(ty: &DB::TypeInfo) -> bool {
<Json<&Self> as Type<DB>>::compatible(ty)
}
}

impl<'q, DB> Encode<'q, DB> for JsonRawValue
where
for<'a> Json<&'a Self>: Encode<'q, DB>,
Expand Down
2 changes: 2 additions & 0 deletions sqlx-mysql/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions sqlx-postgres/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions sqlx-sqlite/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}"),
})
Expand Down
67 changes: 67 additions & 0 deletions tests/any/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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::<Any>().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<TestData> = sqlx::query_scalar("select data from json_test")
.fetch_one(&mut conn)
.await?;

assert_eq!(result.0, test_data);

Ok(())
}
Loading