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
1 change: 1 addition & 0 deletions sqlx-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ features = [
"runtime-tokio",
"migrate",
"any",
"json"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abonander this is the one change that I am somewhat concerned about being "breaking." This change is required because the sqlx-cli is pulling in the json feature even though default-features = false is specified. This causes some weirdness in the compilation only for sqlx-cli and I cannot figure out how to resolve it.

You can see this with cargo tree -p sqlx-cli --format "{p} {f}" -i sqlx-core. The json and any features are being pulled in but the json feature isn't added into the dependency tree in Cargo.toml. This is the only way I could resolve the sqlx-cli issues.

sqlx-core v0.9.0-alpha.1 (sqlx/sqlx-core) _rt-tokio,_tls-native-tls,any,crc,default,json,migrate,native-tls,offline,serde,serde_json,sha2,sqlx-toml,tokio,tokio-stream,toml
├── sqlx v0.9.0-alpha.1 (sqlx) _rt-tokio,_sqlite,any,json,migrate,mysql,postgres,runtime-tokio,sqlite,sqlite-bundled,sqlite-deserialize,sqlite-load-extension,sqlite-unlock-notify,sqlx-mysql,sqlx-postgres,sqlx-sqlite,sqlx-toml,tls-native-tls
│   └── sqlx-cli v0.9.0-alpha.1 (sqlx/sqlx-cli) _sqlite,completions,default,mysql,native-tls,postgres,sqlite,sqlx-toml
├── sqlx-mysql v0.9.0-alpha.1 (sqlx/sqlx-mysql) any,json,migrate,serde
│   └── sqlx v0.9.0-alpha.1 (sqlx) _rt-tokio,_sqlite,any,json,migrate,mysql,postgres,runtime-tokio,sqlite,sqlite-bundled,sqlite-deserialize,sqlite-load-extension,sqlite-unlock-notify,sqlx-mysql,sqlx-postgres,sqlx-sqlite,sqlx-toml,tls-native-tls (*)
├── sqlx-postgres v0.9.0-alpha.1 (sqlx/sqlx-postgres) any,json,migrate
│   └── sqlx v0.9.0-alpha.1 (sqlx) _rt-tokio,_sqlite,any,json,migrate,mysql,postgres,runtime-tokio,sqlite,sqlite-bundled,sqlite-deserialize,sqlite-load-extension,sqlite-unlock-notify,sqlx-mysql,sqlx-postgres,sqlx-sqlite,sqlx-toml,tls-native-tls (*)
└── sqlx-sqlite v0.9.0-alpha.1 (sqlx/sqlx-sqlite) any,bundled,deserialize,json,load-extension,migrate,serde,sqlx-toml,unlock-notify
    └── sqlx v0.9.0-alpha.1 (sqlx) _rt-tokio,_sqlite,any,json,migrate,mysql,postgres,runtime-tokio,sqlite,sqlite-bundled,sqlite-deserialize,sqlite-load-extension,sqlite-unlock-notify,sqlx-mysql,sqlx-postgres,sqlx-sqlite,sqlx-toml,tls-native-tls (*)

]

[features]
Expand Down
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 @@ -57,6 +57,7 @@ impl AnyArguments {
Arc<String>: Type<A::Database> + Encode<'a, A::Database>,
Arc<str>: Type<A::Database> + Encode<'a, A::Database>,
Arc<Vec<u8>>: Type<A::Database> + Encode<'a, A::Database>,
A::Database: AnyJson,
{
let mut out = A::default();

Expand All @@ -71,6 +72,11 @@ impl AnyArguments {
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 @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions sqlx-core/src/any/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<A>(
args: &mut A,
value: Box<serde_json::value::RawValue>,
) -> Result<(), crate::error::BoxDynError>
where
A: crate::arguments::Arguments<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 {}

#[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<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
12 changes: 11 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 Down Expand Up @@ -127,6 +128,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>>();
}
5 changes: 5 additions & 0 deletions sqlx-core/src/any/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub enum AnyValueKind {
Text(Arc<String>),
TextSlice(Arc<str>),
Blob(Arc<Vec<u8>>),
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
Json(Box<serde_json::value::RawValue>),
}

impl AnyValueKind {
Expand All @@ -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,
},
}
}
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
66 changes: 66 additions & 0 deletions tests/any/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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::<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