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",
|
"env_logger",
|
||||||
"futures 0.3.4",
|
"futures 0.3.4",
|
||||||
"paste",
|
"paste",
|
||||||
|
"serde",
|
||||||
"sqlx-core 0.3.0-alpha.1",
|
"sqlx-core 0.3.0-alpha.1",
|
||||||
"sqlx-macros 0.3.0-alpha.1",
|
"sqlx-macros 0.3.0-alpha.1",
|
||||||
"sqlx-test",
|
"sqlx-test",
|
||||||
|
@ -1722,6 +1723,8 @@ dependencies = [
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"percent-encoding 2.1.0",
|
"percent-encoding 2.1.0",
|
||||||
"rand",
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha-1",
|
"sha-1",
|
||||||
"sha2",
|
"sha2",
|
||||||
"tokio 0.2.13",
|
"tokio 0.2.13",
|
||||||
|
|
|
@ -28,7 +28,7 @@ authors = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = [ "tls", "postgres", "mysql", "uuid", "chrono" ]
|
features = [ "tls", "postgres", "mysql", "uuid", "chrono", "json" ]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -50,6 +50,7 @@ bigdecimal = ["sqlx-core/bigdecimal_bigint", "sqlx-macros/bigdecimal"]
|
||||||
chrono = [ "sqlx-core/chrono", "sqlx-macros/chrono" ]
|
chrono = [ "sqlx-core/chrono", "sqlx-macros/chrono" ]
|
||||||
ipnetwork = [ "sqlx-core/ipnetwork", "sqlx-macros/ipnetwork" ]
|
ipnetwork = [ "sqlx-core/ipnetwork", "sqlx-macros/ipnetwork" ]
|
||||||
uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ]
|
uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ]
|
||||||
|
json = [ "sqlx-core/json" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sqlx-core = { version = "0.3.0-alpha.1", path = "sqlx-core", default-features = false }
|
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"
|
trybuild = "1.0.24"
|
||||||
sqlx-test = { path = "./sqlx-test" }
|
sqlx-test = { path = "./sqlx-test" }
|
||||||
paste = "0.1.7"
|
paste = "0.1.7"
|
||||||
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "postgres-macros"
|
name = "postgres-macros"
|
||||||
|
|
|
@ -19,6 +19,7 @@ unstable = []
|
||||||
# `bigdecimal` uses types from it but does not reexport (tsk tsk)
|
# `bigdecimal` uses types from it but does not reexport (tsk tsk)
|
||||||
bigdecimal_bigint = ["bigdecimal", "num-bigint"]
|
bigdecimal_bigint = ["bigdecimal", "num-bigint"]
|
||||||
postgres = [ "md-5", "sha2", "base64", "sha-1", "rand", "hmac", "futures-channel/sink", "futures-util/sink" ]
|
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" ]
|
mysql = [ "sha-1", "sha2", "generic-array", "num-bigint", "base64", "digest", "rand" ]
|
||||||
sqlite = [ "libsqlite3-sys" ]
|
sqlite = [ "libsqlite3-sys" ]
|
||||||
tls = [ "async-native-tls" ]
|
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 }
|
tokio = { version = "0.2.13", default-features = false, features = [ "dns", "fs", "time", "tcp" ], optional = true }
|
||||||
url = { version = "2.1.1", default-features = false }
|
url = { version = "2.1.1", default-features = false }
|
||||||
uuid = { version = "0.8.1", default-features = false, optional = true, features = [ "std" ] }
|
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>
|
# <https://github.com/jgallagher/rusqlite/tree/master/libsqlite3-sys>
|
||||||
[dependencies.libsqlite3-sys]
|
[dependencies.libsqlite3-sys]
|
||||||
|
|
|
@ -9,6 +9,9 @@ pub use listen::{PgListener, PgNotification};
|
||||||
pub use row::{PgRow, PgValue};
|
pub use row::{PgRow, PgValue};
|
||||||
pub use types::PgTypeInfo;
|
pub use types::PgTypeInfo;
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub use types::{Json, Jsonb};
|
||||||
|
|
||||||
mod arguments;
|
mod arguments;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
|
|
@ -62,4 +62,9 @@ impl TypeId {
|
||||||
|
|
||||||
pub(crate) const ARRAY_CIDR: TypeId = TypeId(651);
|
pub(crate) const ARRAY_CIDR: TypeId = TypeId(651);
|
||||||
pub(crate) const ARRAY_INET: TypeId = TypeId(1041);
|
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")]
|
#[cfg(feature = "uuid")]
|
||||||
mod uuid;
|
mod uuid;
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub mod json;
|
||||||
|
|
||||||
#[cfg(feature = "ipnetwork")]
|
#[cfg(feature = "ipnetwork")]
|
||||||
mod ipnetwork;
|
mod ipnetwork;
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub use json::{Json, Jsonb};
|
||||||
|
|
||||||
/// Type information for a Postgres SQL type.
|
/// Type information for a Postgres SQL type.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PgTypeInfo {
|
pub struct PgTypeInfo {
|
||||||
|
@ -149,6 +155,7 @@ impl TypeInfo for PgTypeInfo {
|
||||||
| (TypeId::INET, TypeId::CIDR)
|
| (TypeId::INET, TypeId::CIDR)
|
||||||
| (TypeId::ARRAY_CIDR, TypeId::ARRAY_INET)
|
| (TypeId::ARRAY_CIDR, TypeId::ARRAY_INET)
|
||||||
| (TypeId::ARRAY_INET, TypeId::ARRAY_CIDR) => true,
|
| (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
|
// 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
|
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 sqlx_test::new;
|
||||||
use std::time::Duration;
|
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-async-std", async_std::test)]
|
||||||
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
|
||||||
async fn it_connects() -> anyhow::Result<()> {
|
async fn it_connects() -> anyhow::Result<()> {
|
||||||
|
|
Loading…
Reference in a new issue