-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: PostgreSQL JSON and jsonb types #561
Conversation
This is inspired by the following rust-postgres code: https://github.com/sfackler/rust-postgres/blob/de46ba2176d438ddc0b5d0752a6084f241b22242/postgres-shared/src/types/serde_json.rs Note that we only support encoding version 1, which is all that rust-postgres supports. But this seems to work fine in practice with our database. We can add other encodings as necessary.
This allows us to verify that PostgreSQL actually understands the data that we're generating, and vice versa.
OK, I think we're getting pretty close to a workable prototype! All the tests pass (including the new round-trip tests) and the following compiles: table! {
jsons {
id -> Integer,
data -> Jsonb,
}
} I'm going to try converting more code in one our projects, and see how it works. But first, a quick discussion of why I built this the way I did. Why
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woah, nice! That was quick! Still made it for 2016 ;) And thanks for your in-depth comments! I agree with basically all of it.
I think it will be much easier to implement the first version as a PR against diesel itself. Once it works, we can make the decision about whether to move it to a plugin crate or not.
The reason I originally wanted this as external crate is because the functions and operators #44 describes are pretty Postgres-specific. But this basic support looks like it can also work with SQLite's JSON type. What I am concerned about though, is how the orphan rules might come to bite us. If we define the types in diesel itself, we can't easily add foreign trait impls in an external crate.
Edit: Btw, I'm 75% certain that your code (with namespaces adjusted) would work as an external crate as-is. :)
Why serde_json::Value is a good default JSON representation in Rust
Indeed, AFAIKT serde is the de-facto library of choice here.
Should we support conversion to and from String and Vec using ToSql and FromSql?
Hmm, you mean like update(foo).set(bar.eq(r#"{"baz": true}"#)).execute(&connection)
? I'm not sure if we should go that far. Is that a use case you have? Users can probably replace the string above with r#"{"baz": true}"#.parse()?
and it will work (or something similar; FromStr
works with serde, right?).
What about the JSON query types described in #44 (comment)?
You are right that supporting those and adding support for the basic data type are two different steps. I'm okay with adding basic support and worrying about more later.
Also, I'm not sure how to implement them.
There are infix_predicate!
and sql_function!
that may help.
Is this code ready to merge?
Other than the inline comments (from a quick read-though): Can you add some integration tests? E.g., create a table that has a jsonb column address
, define a #[derive(Serialize, Deserialize)] struct Address { … }
, and do some CRUD work on that column? This could also be done in a doc comment, so we generate a bit of documentation at the same time :)
Also, @sgrif will be back from vacation next week, so I want to wait and see what he thinks about this.
#[derive(Debug, Clone, Copy, Default)] pub struct Json; | ||
|
||
#[cfg(feature = "serde_json")] | ||
/// The JSON SQL type. This type can only be used with `feature = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/JSON/JSONB
, or this is just the same as above :)
Can you maybe add a sentence that JSONB is storing the data in binary and thus more efficiently, and link to https://www.postgresql.org/docs/9.6/static/datatype-json.html? Maybe even quote:
In general, most applications should prefer to store JSON data as jsonb, unless there are quite specialized needs, such as legacy assumptions about ordering of object keys.
use types::{self, ToSql, IsNull, FromSql}; | ||
|
||
primitive_impls!(Json -> (serde_json::Value, pg: (114, 199))); | ||
primitive_impls!(Jsonb -> (serde_json::Value, pg: (3802, 3807))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to add a comment on those magic oid/array_oid numbers here, with links to the postgres spec. But I can't find them, except for in other implementations and the source code…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, my only source for the magic numbers was Googling around and looking at what other libraries did, then confirming that it worked. :-/ I'll keep looking.
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error+Send+Sync>> { | ||
let bytes = not_none!(bytes); | ||
serde_json::from_slice(&bytes) | ||
.map_err(|e| Box::new(e) as Box<Error+Send+Sync>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, is that as Box<Error+Send+Sync
not inferred?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, that does not seem to be the case. I tried removing it and got expected trait std::error::Error, found enum pg::types::json::serde_json::Error
.
impl FromSql<types::Json, Pg> for serde_json::Value { | ||
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error+Send+Sync>> { | ||
let bytes = not_none!(bytes); | ||
serde_json::from_slice(&bytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably unnecessary &
(bytes is already a reference)
Thank you for the detailed review! I'll respond to your detailed comments with either fixes or responses shortly.
Is
No, we have no immediate use case for this. The only thing I can imagine is grabbing raw JSON and sending it directly to a web client without deserializing and re-serializing it. If somebody wants it, they can always add it later. :-) |
I'd love to add better integration tests! Right now, we do have the tests in Where should I add tests that create a table and do more complex experiments? If you can point me at an existing file, I'd be happy to generalize from there. I'd also love to be able to test something like the code below, but I can't get #[cfg(feature = "postgres")]
#[allow(dead_code)]
mod jsonb_compile_test {
extern crate serde_json;
table! {
json_values {
id -> Integer,
value -> Jsonb,
}
}
#[derive(Queryable)]
struct JsonValue {
id: i32,
value: serde_json::Value,
}
fn get_json_value(conn: &diesel::Connection, id: i32)
-> Result<JsonValue, diesel::Error> {
json_values::table.find(id).load::<JsonValue>(conn)
}
} Another patch is incoming soon which should address most of your other comments. |
OK, I've pushed a bunch of fixes suggested in the code review. Thank you for looking at this PR! |
Happy new year!
Oh, good point. Only a few macros are exported. And I'm not sure if we want to expose
If you want to dig even deeper into diesel tests, have a look at For doc tests, I don't think it's easily possible to add custom derive there. You can use the regular macros with the struct definition though. A lot of magic is also done in |
// https://www.postgresql.org/message-id/CA+mi_8Yv2SVOdhAtx-4CbpzoDtaJGkf8QvnushdF8bMgySAbYg@mail.gmail.com | ||
// https://www.postgresql.org/message-id/CA+mi_8bd_g-MDPMwa88w0HXfjysaLFcrCza90+KL9zpRGbxKWg@mail.gmail.com | ||
primitive_impls!(Json -> (serde_json::Value, pg: (114, 199))); | ||
primitive_impls!(Jsonb -> (serde_json::Value, pg: (3802, 3807))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The C interface defines, in server/catalog/pg_type.h
, the macros JSONOID
and JSONBOID
as 114 and 3802, respectively. Not sure why there's no better documentation than that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that sounds like a solid reference :) Maybe we can link to a current release: https://github.com/postgres/postgres/blob/REL9_6_1/src/include/catalog/pg_type.h#L623-L627
(We should probably go through the code and add more references for type OIDs… I hate magic numbers)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seconded. This seems like something that should be added in the pq-sys bindings, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually get these by querying the pg_type
table FWIW. Eventually I'd like to just load these dynamically, as they technically aren't supposed to be assumed stable (in practice they are though other than user defined types so it's very low priority and possibly not even possible with the rest of the project's goals)
Ok merging. |
The master branch is for 0.10, which will launch no earlier than the release of Rust 1.15 which will stabilize Macros 1.1, so that's not a concern. |
Let's not worry about that type for now (or any type that is an extension to any backend). |
I think we should support |
/// - [`serde_json`][Value] | ||
/// | ||
/// [Value]: https://docs.serde.rs/serde_json/value/enum.Value.html | ||
#[derive(Debug, Clone, Copy, Default)] pub struct Jsonb; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should call this type JsonB
and have #[doc(hidden)] pub type Jsonb = JsonB
to make schema inference happy like we do for VarChar?
@@ -13,11 +13,12 @@ dotenv = "0.8.0" | |||
[dependencies] | |||
assert_matches = "1.0.1" | |||
chrono = { version = "^0.2.17" } | |||
diesel = { path = "../diesel", default-features = false, features = ["quickcheck", "chrono", "uuid"] } | |||
diesel = { path = "../diesel", default-features = false, features = ["quickcheck", "chrono", "uuid", "serde_json"] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should add an everything
feature so we use that here (which would also reduce churn in the travis scripts)
@@ -387,6 +387,41 @@ fn pg_uuid_to_sql_uuid() { | |||
|
|||
#[test] | |||
#[cfg(feature = "postgres")] | |||
fn pg_json_from_sql() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mean serde_json_value_from_sql
here.
When I originally wrote #44, I had envisioned that we would have our own enum which was basically a copy of the "json value" enum that both rustc-seriailze and serde provide. However, that was over a year ago, and a lot has changed. I'm fine with just targeting serde. I also envisioned (and would still like to somehow have) an impl that was roughly: impl<T: serde::Deserialize> FromSql<types::Jsonb> for T But that will likely never be possible within Rust's coherence rules until we figure out some method of permitting overlap. Can we see if we can avoid having JSON be treated as a separate type from JSONB internally? I get a result from Overall this code seems fine for the parts that it has tried to accomplish so far. |
Thank you for working on this! I'm excited to hear you guys are experimenting with Diesel. Let me know if there's anything else I can do to help. |
What would you say is left for this PR to be acceptable? Implementing operators, integration tests, and perhaps the |
Yes, it would really help to have a list. I've been procrastinating a bit on the integration tests, because the requested tests for json/jsonb are much more extensive than the existing tests for UUID, so I'm going to have to come up to speed on a fair bit of the code base to implement them. I think operators should not block the list. |
I agree. Integration tests are the most important thing that this PR needs.
It shouldn't be too hard if you find the right things to copy the
boilerplate from. Let me know if you need any help. Otherwise I'll have
some time at the end of the week where I'll do more Diesel stuff, and could
add the Integration tests. (A _lot_ of people will be happy when this
lands!)
Eric Kidd <[email protected]> schrieb am Mo. 9. Jan. 2017 um 23:20:
… Yes, it would really help to have a list.
I've been procrastinating a bit on the integration tests, because the
requested tests for json/jsonb are much more extensive than the existing
tests for UUID, so I'm going to have to come up to speed on a fair bit of
the code base to implement them.
I think operators should *not* block the list.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#561 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AABOX9myXEPOdgPjmxfqTYc6jSofAvy6ks5rQrK0gaJpZM4LYkJK>
.
|
I agree that operators are not essential and should/could come later. If I have some free time, I'll see about making some integration tests for you as well in a PR against this branch. Between all of us, someone should be able to get in an integration test or two :P |
@norcalli Did you write any tests yet? I just wrote a doc test and opened faradayio#1 for it. More tests will be better though :) |
Add doc test for Jsonb type
@killercup I've merged your doc test onto my PR branch. Thank you! |
You're welcome. I'd love to add some more integration tests/doc comments with custom derives using serde (because this will show the roundtrip how its meant to be), but I saw we didn't use codegen in doc comments anywhere. Probably with a good reason; I opened #576 nevertheless. Anyway, I wont get to it today, but I'll try to add a few integration tests tomorrow or on Sunday – unless @norcalli wants to/has time to write some. Do you have any other things you want to add here before landing this, @emk? |
Nope. I'm pretty happy with this personally, but if you want to combine the I'm dealing with an urgent production problem today, unfortunately. |
@emk I have some time this weekend and would love to merge this. The |
So, before this lays around for another week: Let's merge this! We can always tweak it some more later on. |
NOT READY FOR MERGE. Just for comments and feedback. :-)
We've been experimenting with using Diesel in a production application, and we really like the query builder! As discussed in #44, we have an extremely pressing need for PostgreSQL JSON and
jsonb
support, which we currently support using rust-progres.@killercup suggested:
I think it will be much easier to implement the first version as a PR against diesel itself. Once it works, we can make the decision about whether to move it to a plugin crate or not.
The current version of the code provides
types::Json
,types::Jsonb
,ToSql
andFromSql
with basic unit tests. We also need more detailed tests indiesel_tests
, and there are a couple of open questions I'm trying to figure out right now. But I thought we should get some actual code to look at and comment on. :-)Your suggestions and comments are very much appreciated!