How to store arbitrary JSON object in sqlite using Diesel - rust

I have an input JSON:
{"key1": "val1", "key2": 1}
and I want to store it in an sqlite DB to later respond to some API request with the exact same value.
This is my migration:
CREATE TABLE my_table (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
arbitrary_json TEXT NOT NULL
);
My Cargo.toml:
[package]
name = "diesel_playground"
version = "0.1.0"
authors = ["User <user#example.com>"]
edition = "2018"
[dependencies]
diesel = { version = "1.4" , features = ["sqlite"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Using following code:
#[macro_use]
extern crate diesel;
mod schema;
use schema::my_table;
use serde::{Deserialize, Serialize};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::env;
pub fn establish_connection() -> SqliteConnection {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
SqliteConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
#[derive(Debug, Deserialize, Serialize, Queryable, Identifiable)]
#[table_name = "my_table"]
struct Item {
id: i32,
arbitrary_json: serde_json::Value,
}
#[derive(Debug, Deserialize, Serialize, Insertable, Queryable)]
#[table_name = "my_table"]
struct NewItem {
arbitrary_json: serde_json::Value,
}
fn main() {
let my_json = serde_json::json!({
"key1": "val1",
"key2": 1
});
let new_item = NewItem {
arbitrary_json: my_json,
};
let conn = establish_connection();
diesel::insert_into(my_table::table)
.values(&new_item)
.execute(&conn)
.expect("Error adding new item");
let my_item = my_table::table
.find(1)
.first::<Item>(&conn)
.expect("Error selecting id 1");
assert!(my_item.arbitrary_json == new_item.arbitrary_json);
}
I get the following error:
error[E0277]: the trait bound `serde_json::value::Value: diesel::Expression` is not satisfied
--> src/main.rs:27:41
|
27 | #[derive(Debug, Deserialize, Serialize, Insertable, Queryable)]
| ^^^^^^^^^^ the trait `diesel::Expression` is not implemented for `serde_json::value::Value`
I could create a model with String JSON representation and derive From / Into for API input type, but I don't want to insert .into() everywhere in my code now. A DRY solution would to be to do this as I proposed in the code attached.

In my answer I will keep JSON object in the DB in a string representation (schema: TEXT).
For our unsupported type we need following traits implemented: ToSql, FromSql, AsExpression and FromSqlRow.
Now, since one can not implement a trait for a type coming from an external crate it has to be wrapped into a single element tuple:
struct MyJsonType(serde_json::Value)
Now FromSql trait implementation :
impl FromSql<Text, DB> for MyJsonType {
fn from_sql(
bytes: Option<&<diesel::sqlite::Sqlite as Backend>::RawValue>,
) -> deserialize::Result<Self> {
let t = <String as FromSql<Text, DB>>::from_sql(bytes)?;
Ok(Self(serde_json::from_str(&t)?))
}
}
And ToSql trait implementation:
impl ToSql<Text, DB> for MyJsonType {
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
let s = serde_json::to_string(&self.0)?;
<String as ToSql<Text, DB>>::to_sql(&s, out)
}
}
Now the remaining traits can be derived using macros:
#[derive(AsExpression, Debug, Deserialize, Serialize, FromSqlRow)]
#[sql_type = "Text"]
struct MyJsonType(serde_json::Value);
It should be fine to use our new type now:
#[derive(Debug, Deserialize, Serialize, Queryable, Identifiable)]
#[table_name = "my_table"]
struct Item {
id: i32,
arbitrary_json: MyJsonType
}

Related

Rust: sqlx try_from Option<Uuid>

I have the following code:
// cargo.toml:
// serde = { version = "1.0", features = ["derive"] }
// uuid = { version = "1.2", features = ["serde", "v4"] }
// sqlx = { version = "0.6.2", features = ["runtime-async-std-native-tls", "sqlite", "postgres", "chrono", "uuid", "macros"]}
use uuid::{Uuid, fmt::Hyphenated};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, FromRow, PartialEq)]
pub struct Transaction {
#[sqlx(try_from = "Hyphenated")]
pub t_id: Uuid,
#[sqlx(try_from = "Option<Hyphenated>")]
pub corr_id: Option<Uuid>,
}
In SQLite database, id stored in hyphenated format like "550e8400-e29b-41d4-a716-446655440000". t_id not null, corr_id nullable.
macro #[sqlx(try_from = "Hyphenated")] works fine, but I cant figure out, how to use it with Option for corr_id field. Given code panics. Any help is greatly appreciated.
The compiler tells us that it cannot implicitly convert Hyphenated to an Option<Uuid>,
| #[derive(Debug, Serialize, Deserialize, FromRow, PartialEq)]
| ^^^^^^^ the trait `From<Hyphenated>` is not implemented for `std::option::Option<Uuid>`
and we can't implement external traits for external types. The only choices left seem to be
1. Implement the FromRow trait yourself
Since we know that we'll be using SQLite and the corr_id is a nullable text column, we can implement FromRow for sqlx::sqlite::SqliteRows. If your struct (row) only has these two fields, this is fine but when extending it with additional fields, you'll need to update your FromRow implementation as well.
use sqlx::{sqlite::SqliteRow, FromRow, Row};
use uuid::{fmt::Hyphenated, Uuid};
pub struct Transaction {
pub t_id: Uuid,
pub corr_id: Option<Uuid>,
}
impl<'r> FromRow<'r, SqliteRow> for Transaction {
fn from_row(row: &'r SqliteRow) -> Result<Self, sqlx::Error> {
let t_id: Hyphenated = row.try_get("t_id")?;
let corr_id: &str = row.try_get("corr_id")?;
let corr_id = if corr_id.is_empty() {
None
} else {
let uuid = Uuid::try_parse(&corr_id).map_err(|e| sqlx::Error::ColumnDecode {
index: "corr_id".to_owned(),
source: Box::new(e),
})?;
Some(uuid)
};
Ok(Transaction {
t_id: t_id.into(),
corr_id,
})
}
}
2. Use a newtype
This way, you can reuse your "nullable" type in other structs if necessary, and can even implement Deref, if you want to make extracting the inner UUID easier. It does come with some extra allocations though, since the incoming bytes are converted first to String, then parsed into Uuid.
use std::ops::Deref;
use sqlx::FromRow;
#[derive(FromRow)]
pub struct Transaction {
#[sqlx(try_from = "Hyphenated")]
pub t_id: Uuid,
#[sqlx(try_from = "String")]
pub corr_id: NullableUuid,
}
pub struct NullableUuid(Option<Uuid>);
impl TryFrom<String> for NullableUuid {
type Error = uuid::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
let inner = if value.is_empty() {
None
} else {
let uuid = Uuid::try_parse(&value)?;
Some(uuid)
};
Ok(NullableUuid(inner))
}
}
impl Deref for NullableUuid {
type Target = Option<Uuid>;
fn deref(&self) -> &Self::Target {
&self.0
}
}

is it possible to use enum to parse the request in rust rocket

I want to do a condition query in rust diesel(diesel = { version = "1.4.7", features = ["postgres","64-column-tables","chrono"] }). read the docs from here and write the fuction like this:
fn find_channel(request: &ChannelRequest) -> Box<dyn BoxableExpression<crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::table, DB, SqlType=Bool> + '_> {
use crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::*;
match request {
ChannelRequest::editorPick(editorPick) => Box::new(crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::editor_pick.eq(editorPick)),
_ => Box::new(crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::editor_pick.eq(0))
}
}
but the function need the parameter was enum, and this is the ChannelRequest define in rocket rocket = { version = "0.5.0-rc.1", features = ["json"] } :
use rocket::serde::Deserialize;
use rocket::serde::Serialize;
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub enum ChannelRequest {
userId(Option<i64>),
pageNum(Option<i64>),
pageSize(Option<i64>),
editorPick(Option<i32>),
}
and this is the rocket api controller define:
#[post("/v1/page", data = "<request>")]
pub fn page(request: Json<ChannelRequest>) -> content::Json<String> {
let channels = channel_query::<Vec<RssSubSource>>(&request);
return box_rest_response(channels);
}
and this is the channel_query which invoke the conditional query:
pub fn channel_query<T>(request: &Json<ChannelRequest>) -> PaginationResponse<Vec<RssSubSource>> {
use crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::*;
let connection = config::establish_connection();
let query = rss_sub_source
.filter(find_channel(&request.0))
.order(created_time.desc())
.paginate(1)
.per_page(10);
let query_result: QueryResult<(Vec<_>, i64, i64)> = query.load_and_count_pages_total::<RssSubSource>(&connection);
let page_result = map_pagination_res(
query_result,
1,
10);
return page_result;
}
when I request the channel search api, seems the server side did not understand the client request, what should I do to using enum to receive the client request? is it possible? or what should I do to tweak the condition query function to make it work?
I tweak the match data type frome enum to struct, change the enum to strunct:
use rocket::serde::Deserialize;
use rocket::serde::Serialize;
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct ChannelRequest {
pub userId: Option<i64>,
pub pageNum: Option<i64>,
pub pageSize: Option<i64>,
pub editorPick: Option<i32>
}
and tweak the condition query like this:
fn find_channel(request: &ChannelRequest) -> Box<dyn BoxableExpression<crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::table, DB, SqlType=Bool> + '_> {
use crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::*;
match request {
ChannelRequest { editorPick, .. } => Box::new(crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::editor_pick.eq(editorPick)),
_ => Box::new(crate::model::diesel::dolphin::dolphin_schema::rss_sub_source::dsl::editor_pick.eq(0))
}
}

error: cannot find attribute `table_name` in this scope

I want to do a page query using rust diesel, I am using this code to do a unit test in rust:
#[cfg(test)]
mod test {
use std::env;
use diesel::{Connection, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl};
use rust_wheel::common::query::pagination::PaginateForQuerySource;
use crate::model::diesel::rhythm::rhythm_schema::favorites::dsl::favorites;
use crate::model::diesel::rhythm::rhythm_schema::favorites::like_status;
use crate::models::Favorites;
#[test]
fn page_test(){
use crate::model::diesel::rhythm::rhythm_schema::favorites::dsl::*;
use rust_wheel::common::query::pagination::{PaginateForQueryFragment, PaginateForQuerySource};
let conn = establish_music_connection();
let query = favorites
.filter(like_status.eq(1))
.paginate(1)
.per_page(10)
.load::<Favorites>(&conn)
.expect("query fav failed");
println!("{:?}", 1);
}
pub fn establish_music_connection() -> PgConnection {
let database_url = std::env::var("MUSIC_DATABASE_URL").expect("MUSIC_DATABASE_URL must be set");
PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
}
}
shows error like this:
error: cannot find attribute `table_name` in this scope
--> src/models.rs:15:3
|
15 | #[table_name = "favorites"]
| ^^^^^^^^^^
this is my models.rs define, this two models are store in different database:
use rocket::serde::Serialize;
use serde::Deserialize;
use crate::model::diesel::dolphin::dolphin_schema::dashboard;
use crate::model::diesel::rhythm::rhythm_schema::favorites;
#[derive(Insertable, Serialize, Queryable, Deserialize,Default)]
#[table_name = "dashboard"]
pub struct Dashboard {
pub id: i32,
pub app_count: i32,
pub user_count: i32
}
#[derive(Serialize, Queryable, Deserialize,Default)]
#[table_name = "favorites"]
pub struct Favorites {
pub id: i64,
pub song_id: String,
pub created_time: i64
}
why did this happen? what should I do to fix it?
Only the Insertable derive macro handles the #[table_name = ...] attribute. So it should not be included if you're not using it.

Serde tag = x, but keep the tag in the struct

I'm trying to de-serialise JSON to a Rust struct with enum variants using internally tagged JSON (https://serde.rs/enum-representations.html).
I want to store the tag of the variant inside the struct - currently serde stores this data in attributes.
Can this be done keeping the tag key inside the struct?
The methods I have tried:
A. #[serde(untagged)]
This works but I want to avoid it because of the performance hit of searching for a pattern match.
B. #[serde(tag = "f_tag")]
This does not work and results in "duplicate field `f_tag`".
Both the serde attribute and the enum variant rename try to use the same key.
I do not want to place #[serde(rename = "f_tag")] under Uni as this defines the tag key in the parent enum. (I want child structs to have the same tag key anywhere they are contained inside a parent enum).
Example:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=584c3f32488a1a27f47dac9a0e81d31c
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{Result, Value};
#[derive(Serialize, Deserialize, Debug)]
struct S1 {
f1: String,
f2: Uni,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "f_tag")]
// #[serde(untagged)]
enum Uni {
String(String),
// #[serde(rename = "f_tag")]
S2(S2),
// #[serde(rename = "f_tag")]
S3(S3),
}
#[derive(Serialize, Deserialize, Debug)]
struct S2 {
f_tag: S2Tag,
f2_s2: bool,
}
#[derive(Serialize, Deserialize, Debug)]
struct S3 {
f_tag: S3Tag,
f2_s3: bool,
}
#[derive(Serialize, Deserialize, Debug)]
enum S2Tag {
#[serde(rename = "s2")]
S2,
}
#[derive(Serialize, Deserialize, Debug)]
enum S3Tag {
#[serde(rename = "s3")]
S3,
}
fn main() {
let s1 = S1 {
f1: "s1.f1".into(),
f2: Uni::S2(S2 {
f_tag: S2Tag::S2,
f2_s2: true,
}),
};
let j = serde_json::to_string(&s1).unwrap();
dbg!(&j);
let s1_2: S1 = serde_json::from_str(&j).unwrap();
dbg!(s1_2);
}
{
"f1": "s1.f1",
"f2": {
// Issue: serde(tag = x) uses the name of the enum here.
"f_tag": "S2",
"f_tag": "s2",
"f2_s2": true
}
}

Deserializing TOML into vector of enum with values

I'm trying to read a TOML file to create a struct that contains a vector of enums with associated values. Here's the sample code:
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate toml;
use std::fs::File;
use std::io::Read;
#[derive(Debug, Deserialize, PartialEq)]
struct Actor {
name: String,
actions: Vec<Actions>,
}
#[derive(Debug, Deserialize, PartialEq)]
enum Actions {
Wait(usize),
Move { x: usize, y: usize },
}
fn main() {
let input_file = "./sample_actor.toml";
let mut file = File::open(input_file).unwrap();
let mut file_content = String::new();
let _bytes_read = file.read_to_string(&mut file_content).unwrap();
let actor: Actor = toml::from_str(&file_content).unwrap();
println!("Read actor {:?}", actor);
}
Cargo.toml
[dependencies]
serde_derive = "1.0.10"
serde = "1.0.10"
toml = "0.4.2"
sample_actor.toml
name = "actor1"
actions = [move 3 4, wait 20, move 5 6]
I know the file looks "wrong", but I have no idea how I should write the actions in the TOML file such that the crate would be able to recognize them as an enum with X number of values.
The error I get when running this example with cargo run is the following:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { inner: ErrorInner { kind: NumberInvalid, line: Some(1), col: 11, message: "", key: [] } }', /checkout/src/libcore/result.rs:906:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.
I know that I probably need to implement FromStr for my enum to convert a string into my enum, and I briefly know that custom deserializers can be implemented to deserialize in a specific way, but I'm not sure how these pieces fit together.
It seems an equivalent example using serde_json instead of TOML works straight out (using JSON files instead of TOML of course).
JSON version of the code:
extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;
use serde_json::Error;
use std::fs::File;
use std::io::Read;
#[derive(Debug, Serialize, Deserialize)]
enum Foo {
bar(u32),
baz { x: u32, y: u32 },
}
#[derive(Debug, Serialize, Deserialize)]
struct Address {
street: String,
city: String,
nums: Vec<Foo>,
}
fn main() {
/*
let address = Address {
street: "10 Downing Street".to_owned(),
city: "London".to_owned(),
nums : vec![Foo::bar(1), Foo::baz{x : 2, y : 3}],
};
// Serialize it to a JSON string.
let j = serde_json::to_string(&address).unwrap();
// Print, write to a file, or send to an HTTP server.
println!("{}", j);
*/
let input_file = "./sample_address.json";
let mut file = File::open(input_file).unwrap();
let mut file_content = String::new();
let _bytes_read = file.read_to_string(&mut file_content).unwrap();
let address: Address = serde_json::from_str(&file_content).unwrap();
println!("{:?}", address);
}
The JSON data read/written in this example is:
Address { street: "10 Downing Street", city: "London", nums: [bar(1), baz { x: 2, y: 3 }] }
Maybe the TOML crate can't support my use-case?
Serde has lots of options for serializing enums. One that works for your case:
use serde::{Deserialize, Serialize}; // 1.0.117
use toml; // 0.5.7
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "args")]
enum Actions {
Wait(usize),
Move { x: usize, y: usize },
}
fn main() {
let a_wait = Actions::Wait(5);
println!("{}", toml::to_string(&a_wait).unwrap());
let a_move = Actions::Move { x: 1, y: 1 };
println!("{}", toml::to_string(&a_move).unwrap());
}
type = "Wait"
args = 5
type = "Move"
[args]
x = 1
y = 1

Resources