diff --git a/Cargo.lock b/Cargo.lock index aaa5a44291..c4dd0d5358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2150,6 +2150,21 @@ dependencies = [ "serde", ] +[[package]] +name = "serialize" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "dotenv", + "futures", + "paw", + "serde", + "serde_json", + "sqlx", + "structopt", +] + [[package]] name = "sha-1" version = "0.9.3" diff --git a/Cargo.toml b/Cargo.toml index ea93952103..445120a975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "examples/postgres/listen", "examples/postgres/todos", "examples/postgres/mockable-todos", + "examples/postgres/serialize", "examples/sqlite/todos", ] @@ -92,6 +93,7 @@ time = [ "sqlx-core/time", "sqlx-macros/time" ] bit-vec = [ "sqlx-core/bit-vec", "sqlx-macros/bit-vec"] bstr = [ "sqlx-core/bstr" ] git2 = [ "sqlx-core/git2" ] +serialize = [ "sqlx-core/serialize", "sqlx-macros/serialize" ] [dependencies] sqlx-core = { version = "0.5.1", path = "sqlx-core", default-features = false } diff --git a/examples/postgres/serialize/Cargo.toml b/examples/postgres/serialize/Cargo.toml new file mode 100644 index 0000000000..990b6eb37b --- /dev/null +++ b/examples/postgres/serialize/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "serialize" +version = "0.1.0" +edition = "2018" +workspace = "../../../" + +[dependencies] +anyhow = "1.0" +async-std = { version = "1.6.0", features = [ "attributes" ] } +dotenv = "0.15.0" +futures = "0.3" +paw = "1.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { path = "../../../", features = ["runtime-async-std-rustls", "postgres", "json", "serialize"] } +structopt = { version = "0.3", features = ["paw"] } diff --git a/examples/postgres/serialize/README.md b/examples/postgres/serialize/README.md new file mode 100644 index 0000000000..4163709fac --- /dev/null +++ b/examples/postgres/serialize/README.md @@ -0,0 +1,47 @@ +# JSON Example using serialize feature + +When the serialize feature is enabled, the query!() macro returns a +struct that implements serde::Serialize. This means that each 'Row' +value can be converted to json text using serde_json::to_string(&row). +This includes nested 'jsonb', such as the person column in this +example. + +## Setup + +1. Declare the database URL + + ``` + export DATABASE_URL="postgres://postgres:password@localhost/serialize" + ``` + +2. Create the database. + + ``` + $ sqlx db create + ``` + +3. Run sql migrations + + ``` + $ sqlx migrate run + ``` + +## Usage + +Add a person + +``` +echo '{ "name": "John Doe", "age": 30 }' | cargo run -- add +``` + +or with extra keys + +``` +echo '{ "name": "Jane Doe", "age": 25, "array": ["string", true, 0] }' | cargo run -- add +``` + +List all people + +``` +cargo run +``` diff --git a/examples/postgres/serialize/migrations/20200824190010_json.sql b/examples/postgres/serialize/migrations/20200824190010_json.sql new file mode 100644 index 0000000000..02458ff72d --- /dev/null +++ b/examples/postgres/serialize/migrations/20200824190010_json.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS people +( + id BIGSERIAL PRIMARY KEY, + person JSONB NOT NULL +); diff --git a/examples/postgres/serialize/src/main.rs b/examples/postgres/serialize/src/main.rs new file mode 100644 index 0000000000..f063ae66ca --- /dev/null +++ b/examples/postgres/serialize/src/main.rs @@ -0,0 +1,91 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sqlx::postgres::PgPool; +use sqlx::types::Json; +use std::io::{self, Read}; +use std::num::NonZeroU8; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(subcommand)] + cmd: Option, +} + +#[derive(StructOpt)] +enum Command { + Add, +} + +#[derive(Deserialize, Serialize)] +struct Person { + name: String, + age: NonZeroU8, + #[serde(flatten)] + extra: Map, +} + +#[async_std::main] +#[paw::main] +async fn main(args: Args) -> anyhow::Result<()> { + let pool = PgPool::connect(&dotenv::var("DATABASE_URL")?).await?; + + match args.cmd { + Some(Command::Add) => { + let mut json = String::new(); + io::stdin().read_to_string(&mut json)?; + + let person: Person = serde_json::from_str(&json)?; + println!( + "Adding new person: {}", + &serde_json::to_string_pretty(&person)? + ); + + let person_id = add_person(&pool, person).await?; + println!("Added new person with ID {}", person_id); + } + None => { + println!("{}", list_people(&pool).await?); + } + } + + Ok(()) +} + +async fn add_person(pool: &PgPool, person: Person) -> anyhow::Result { + let rec = sqlx::query!( + r#" +INSERT INTO people ( person ) +VALUES ( $1 ) +RETURNING id + "#, + Json(person) as _ + ) + .fetch_one(pool) + .await?; + + Ok(rec.id) +} + +async fn list_people(pool: &PgPool) -> anyhow::Result { + let mut buf = String::from("["); + for (i, row) in sqlx::query!( + r#" +SELECT id, person +FROM people +ORDER BY id + "# + ) + .fetch_all(pool) + .await? + .iter() + .enumerate() + { + if i > 0 { + buf.push_str(",\n"); + } + buf.push_str(&serde_json::to_string_pretty(&row)?); + } + buf.push_str("]\n"); + Ok(buf) +} diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index 1a7b795cec..b8310b0567 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -32,6 +32,7 @@ all-types = [ "chrono", "time", "bigdecimal", "decimal", "ipnetwork", "json", "u bigdecimal = [ "bigdecimal_", "num-bigint" ] decimal = [ "rust_decimal", "num-bigint" ] json = [ "serde", "serde_json" ] +serialize = [ "serde", "serde_json" ] # runtimes runtime-actix-native-tls = [ "sqlx-rt/runtime-actix-native-tls", "_tls-native-tls", "_rt-actix" ] diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 0310a39fe2..85def82236 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -51,6 +51,7 @@ ipnetwork = [ "sqlx-core/ipnetwork" ] uuid = [ "sqlx-core/uuid" ] bit-vec = [ "sqlx-core/bit-vec" ] json = [ "sqlx-core/json", "serde_json" ] +serialize = [ "serde", "serde_json" ] [dependencies] dotenv = { version = "0.15.0", default-features = false } diff --git a/sqlx-macros/src/query/mod.rs b/sqlx-macros/src/query/mod.rs index 2c1a71544d..d5b5a37184 100644 --- a/sqlx-macros/src/query/mod.rs +++ b/sqlx-macros/src/query/mod.rs @@ -278,12 +278,19 @@ where }| quote!(#ident: #type_,), ); - let mut record_tokens = quote! { + let mut record_tokens = TokenStream::new(); + + #[cfg(feature = "serialize")] + record_tokens.extend(quote! { + #[derive(serde::Serialize)] + }); + + record_tokens.extend(quote! { #[derive(Debug)] struct #record_name { #(#record_fields)* } - }; + }); record_tokens.extend(output::quote_query_as::( &input, diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index dee9062d8e..055a4dd4d8 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -887,3 +887,14 @@ from (values (null)) vals(val) Ok(()) } + +#[cfg(feature = "serialize")] +#[sqlx_macros::test] +async fn it_row_serializes_to_json() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let json = serde_json::to_string(&sqlx::query!(" select (1) as id ").fetch_all(&pool).await?)?; + assert_eq!(&json, r#"[{"id":1}]"#); + + Ok(()) +}