mirror of
https://github.com/launchbadge/sqlx
synced 2024-09-20 14:21:57 +00:00
First draft of JSON/JSONB support for postgres
This introduces two new wrapper types `Json` and `Jsonb`, currently exported by `sqlx::postgres::{Json, Jsonb}` and adds serde and serde_json as optional dependencies, under the feature flag `json`. Wrapping types in `Json[b]` that implement `serde::Deserialize` and `serde::Serialize` allows them to be decoded and encoded respectivly.
This commit is contained in:
parent
cbdc1bbfb2
commit
b7e53e885a
9 changed files with 185 additions and 1 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1656,6 +1656,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"futures 0.3.4",
|
||||
"paste",
|
||||
"serde",
|
||||
"sqlx-core 0.3.0-alpha.1",
|
||||
"sqlx-macros 0.3.0-alpha.1",
|
||||
"sqlx-test",
|
||||
|
@ -1722,6 +1723,8 @@ dependencies = [
|
|||
"num-bigint",
|
||||
"percent-encoding 2.1.0",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha-1",
|
||||
"sha2",
|
||||
"tokio 0.2.13",
|
||||
|
|
|
@ -28,7 +28,7 @@ authors = [
|
|||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = [ "tls", "postgres", "mysql", "uuid", "chrono" ]
|
||||
features = [ "tls", "postgres", "mysql", "uuid", "chrono", "json" ]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
|
@ -50,6 +50,7 @@ bigdecimal = ["sqlx-core/bigdecimal_bigint", "sqlx-macros/bigdecimal"]
|
|||
chrono = [ "sqlx-core/chrono", "sqlx-macros/chrono" ]
|
||||
ipnetwork = [ "sqlx-core/ipnetwork", "sqlx-macros/ipnetwork" ]
|
||||
uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ]
|
||||
json = [ "sqlx-core/json" ]
|
||||
|
||||
[dependencies]
|
||||
sqlx-core = { version = "0.3.0-alpha.1", path = "sqlx-core", default-features = false }
|
||||
|
@ -65,6 +66,7 @@ dotenv = "0.15.0"
|
|||
trybuild = "1.0.24"
|
||||
sqlx-test = { path = "./sqlx-test" }
|
||||
paste = "0.1.7"
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
|
||||
[[test]]
|
||||
name = "postgres-macros"
|
||||
|
|
|
@ -19,6 +19,7 @@ unstable = []
|
|||
# `bigdecimal` uses types from it but does not reexport (tsk tsk)
|
||||
bigdecimal_bigint = ["bigdecimal", "num-bigint"]
|
||||
postgres = [ "md-5", "sha2", "base64", "sha-1", "rand", "hmac", "futures-channel/sink", "futures-util/sink" ]
|
||||
json = ["serde", "serde_json"]
|
||||
mysql = [ "sha-1", "sha2", "generic-array", "num-bigint", "base64", "digest", "rand" ]
|
||||
sqlite = [ "libsqlite3-sys" ]
|
||||
tls = [ "async-native-tls" ]
|
||||
|
@ -56,6 +57,8 @@ sha2 = { version = "0.8.1", default-features = false, optional = true }
|
|||
tokio = { version = "0.2.13", default-features = false, features = [ "dns", "fs", "time", "tcp" ], optional = true }
|
||||
url = { version = "2.1.1", default-features = false }
|
||||
uuid = { version = "0.8.1", default-features = false, optional = true, features = [ "std" ] }
|
||||
serde = { version = "1.0", features = [ "derive" ], optional = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
|
||||
# <https://github.com/jgallagher/rusqlite/tree/master/libsqlite3-sys>
|
||||
[dependencies.libsqlite3-sys]
|
||||
|
|
|
@ -9,6 +9,9 @@ pub use listen::{PgListener, PgNotification};
|
|||
pub use row::{PgRow, PgValue};
|
||||
pub use types::PgTypeInfo;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub use types::{Json, Jsonb};
|
||||
|
||||
mod arguments;
|
||||
mod connection;
|
||||
mod cursor;
|
||||
|
|
|
@ -62,4 +62,9 @@ impl TypeId {
|
|||
|
||||
pub(crate) const ARRAY_CIDR: TypeId = TypeId(651);
|
||||
pub(crate) const ARRAY_INET: TypeId = TypeId(1041);
|
||||
|
||||
// JSON
|
||||
|
||||
pub(crate) const JSON: TypeId = TypeId(114);
|
||||
pub(crate) const JSONB: TypeId = TypeId(3802);
|
||||
}
|
||||
|
|
93
sqlx-core/src/postgres/types/json.rs
Normal file
93
sqlx-core/src/postgres/types/json.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use crate::decode::{Decode, DecodeError};
|
||||
use crate::encode::Encode;
|
||||
use crate::io::{Buf, BufMut};
|
||||
use crate::postgres::protocol::TypeId;
|
||||
use crate::postgres::types::PgTypeInfo;
|
||||
use crate::postgres::Postgres;
|
||||
use crate::types::HasSqlType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
impl HasSqlType<Value> for Postgres {
|
||||
fn type_info() -> PgTypeInfo {
|
||||
PgTypeInfo::new(TypeId::JSON)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<Postgres> for Value {
|
||||
fn encode(&self, buf: &mut Vec<u8>) {
|
||||
Json(self).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<Postgres> for Value {
|
||||
fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
|
||||
let Json(item) = Decode::decode(buf)?;
|
||||
Ok(item)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Json<T>(pub T);
|
||||
|
||||
impl<T> HasSqlType<Json<T>> for Postgres {
|
||||
fn type_info() -> PgTypeInfo {
|
||||
PgTypeInfo::new(TypeId::JSON)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Encode<Postgres> for Json<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn encode(&self, buf: &mut Vec<u8>) {
|
||||
serde_json::to_writer(buf, &self.0)
|
||||
.expect("failed to serialize json for encoding to database");
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Decode<Postgres> for Json<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
|
||||
let item = serde_json::from_slice(buf)?;
|
||||
Ok(Json(item))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Jsonb<T>(pub T);
|
||||
|
||||
impl<T> HasSqlType<Jsonb<T>> for Postgres {
|
||||
fn type_info() -> PgTypeInfo {
|
||||
PgTypeInfo::new(TypeId::JSONB)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Encode<Postgres> for Jsonb<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn encode(&self, buf: &mut Vec<u8>) {
|
||||
// TODO: I haven't been figure out what this byte is, but it is required or else we get the error:
|
||||
// Error: unsupported jsonb version number 34
|
||||
buf.put_u8(1);
|
||||
|
||||
serde_json::to_writer(buf, &self.0)
|
||||
.expect("failed to serialize json for encoding to database");
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Decode<Postgres> for Jsonb<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
fn decode(mut buf: &[u8]) -> Result<Self, DecodeError> {
|
||||
// TODO: I don't know what this byte is, similarly to Encode
|
||||
let _ = buf.get_u8()?;
|
||||
|
||||
let item = serde_json::from_slice(buf)?;
|
||||
Ok(Jsonb(item))
|
||||
}
|
||||
}
|
|
@ -80,9 +80,15 @@ mod chrono;
|
|||
#[cfg(feature = "uuid")]
|
||||
mod uuid;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub mod json;
|
||||
|
||||
#[cfg(feature = "ipnetwork")]
|
||||
mod ipnetwork;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub use json::{Json, Jsonb};
|
||||
|
||||
/// Type information for a Postgres SQL type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PgTypeInfo {
|
||||
|
@ -149,6 +155,7 @@ impl TypeInfo for PgTypeInfo {
|
|||
| (TypeId::INET, TypeId::CIDR)
|
||||
| (TypeId::ARRAY_CIDR, TypeId::ARRAY_INET)
|
||||
| (TypeId::ARRAY_INET, TypeId::ARRAY_CIDR) => true,
|
||||
|
||||
_ => {
|
||||
// TODO: 99% of postgres types are direct equality for [compatible]; when we add something that isn't (e.g, JSON/JSONB), fix this here
|
||||
self.id.0 == other.id.0
|
||||
|
|
66
tests/postgres-types-json.rs
Normal file
66
tests/postgres-types-json.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use sqlx::{postgres::{PgConnection, Json, Jsonb}, Connection as _, Row};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
async fn connect() -> anyhow::Result<PgConnection> {
|
||||
Ok(PgConnection::open(dotenv::var("DATABASE_URL")?).await?)
|
||||
}
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $ty:ty: $($text:literal == $value:expr),+) => {
|
||||
mod $name {
|
||||
use super::*;
|
||||
|
||||
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
|
||||
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
||||
async fn json () -> anyhow::Result<()> {
|
||||
let mut conn = connect().await?;
|
||||
|
||||
// Always use jsonb for the comparison, as json does not support equality
|
||||
$(
|
||||
let row = sqlx::query(&format!("SELECT {}::json::jsonb = $1::jsonb, $1 as _1", $text))
|
||||
.bind(Json($value))
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
assert!(row.get::<bool, _>(0));
|
||||
assert!(Json($value) == row.get::<Json<$ty>, _>("_1"));
|
||||
)+
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
|
||||
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
||||
async fn jsonb () -> anyhow::Result<()> {
|
||||
let mut conn = connect().await?;
|
||||
|
||||
$(
|
||||
let row = sqlx::query(&format!("SELECT {}::jsonb = $1::jsonb, $1 as _1", $text))
|
||||
.bind(Jsonb($value))
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
assert!(row.get::<bool, _>(0));
|
||||
assert!(Jsonb($value) == row.get::<Jsonb<$ty>, _>("_1"));
|
||||
)+
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test!(postgres_json_string: String: "'\"Hello, World!\"'" == "Hello, World!".to_string());
|
||||
|
||||
test!(postgres_json_emoji_simple: String: "'\"😎\"'" == "😎".to_string());
|
||||
test!(postgres_json_emoji_multi: String: "'\"🙋♀️\"'" == "🙋♀️".to_string());
|
||||
|
||||
test!(postgres_json_vec: Vec<String>: "'[\"Hello\", \"World!\"]'" == vec!["Hello".to_string(), "World!".to_string()]);
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
struct Friend {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
test!(postgres_json_struct: Friend: "'{\"name\":\"Joe\",\"age\":33}'" == Friend { name: "Joe".to_string(), age: 33 });
|
||||
|
|
@ -4,6 +4,8 @@ use sqlx::{Connection, Executor, Postgres, Row};
|
|||
use sqlx_test::new;
|
||||
use std::time::Duration;
|
||||
|
||||
// TODO: As soon as I tried to deserialize a json value in a function, inferance for this test stopped working. I am at a loss as to how to resolve this.
|
||||
#[cfg(not(feature = "json"))]
|
||||
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
|
||||
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
||||
async fn it_connects() -> anyhow::Result<()> {
|
||||
|
|
Loading…
Reference in a new issue