Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
Copy Markdown
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;
Comment thread
JosiahParry marked this conversation as resolved.
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
9 changes: 9 additions & 0 deletions sqlx-mysql/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ impl<'a> TryFrom<&'a MySqlTypeInfo> for AnyTypeInfo {
ColumnType::LongLong => AnyTypeInfoKind::BigInt,
ColumnType::Float => AnyTypeInfoKind::Real,
ColumnType::Double => AnyTypeInfoKind::Double,
ColumnType::Blob
| ColumnType::TinyBlob
| ColumnType::MediumBlob
| ColumnType::LongBlob => AnyTypeInfoKind::Blob,
ColumnType::String | ColumnType::VarString | ColumnType::VarChar => {
AnyTypeInfoKind::Text
}
#[cfg(feature = "json")]
ColumnType::Json => AnyTypeInfoKind::Json,
// Checks for any applicable type and compatible collations
_ if <str as Type<MySql>>::compatible(type_info) => AnyTypeInfoKind::Text,
_ if <[u8] as Type<MySql>>::compatible(type_info) => AnyTypeInfoKind::Blob,
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 @@ -200,6 +200,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 @@ -224,6 +224,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 @@ -205,6 +210,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