Skip to content
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

Add support for Uuid to SQLite #364

Closed
flosse opened this issue Jul 1, 2016 · 18 comments
Closed

Add support for Uuid to SQLite #364

flosse opened this issue Jul 1, 2016 · 18 comments

Comments

@flosse
Copy link

flosse commented Jul 1, 2016

It would be nice to be able to use uuid::Uuid within the structs in combination of SQLite!

@sgrif
Copy link
Member

sgrif commented Jul 1, 2016

As far as I'm aware there is no data type in SQLite that maps to UUID?

@flosse
Copy link
Author

flosse commented Jul 1, 2016

As far as I'm aware there is no data type in SQLite that maps to UUID?

I'm sorry. You're totally right!

@flosse flosse closed this as completed Jul 1, 2016
@theduke
Copy link
Contributor

theduke commented Apr 9, 2017

No there is not, but the UUID could just be (de)serialized to BINARY or TEXT, which would be much nicer than having to convert manually. @sgrif

@0xcaff
Copy link

0xcaff commented May 23, 2018

I've implemented storing UUIDs in SQLite using BINARY(128) and a wrapper type in my package.

Code:
https://github.com/forte-music/core/blob/fc9cd6217708b0dd6ae684df3a53276804479c59/src/models/id.rs#L67-L121

SQL:
https://github.com/forte-music/core/blob/feature/sync-engine/migrations/2018-05-14-200933_setup/up.sql#L1-L12

Usage:
https://github.com/forte-music/core/blob/fc9cd6217708b0dd6ae684df3a53276804479c59/src/models/album.rs#L10-L21

I can make a pull request with an adaptation of this code if anyone is interested.

@sameer
Copy link

sameer commented Aug 15, 2018

I'm interested @0xcaff, would appreciate it if you have the time to do a PR for this.

@0xcaff
Copy link

0xcaff commented Aug 15, 2018

I’d like thoughts from a contributor before making a PR. Thoughts @sgrif?

@weiznich
Copy link
Member

Our policy for including mappings between certain SQL types an rust types is like following:

  • Either a rust type is able to represent all possible values of the matched SQL type and also the SQL type could represent all values of the rust type
  • Or the type is a fundamental type like boolean.
    The first condition is not fulfilled because there at Text and also Binary values that are no valid uuid's (for example all shorter or longer values)
    In my opinion also the second point is not fulfilled because uuid is not that fundamental like for example booleans. (If anyone from @diesel/core disagrees, pleas response here 😉)

Given that (and given that it is quite easy to write a new type wrapper for this outside of diesel) I would say that impl should not be part of diesel itself.

@0xcaff
Copy link

0xcaff commented Aug 15, 2018

Playing devils advocate here.

I'd argue that uuid is a fundamental type. Many applications use the uuid create and diesel already has support for using it with some SQL dialects (postgres). It's useful to have a built, tested and discoverable implementation for users of diesel. Writing a new type wrapper is annoying because of the need for boilerplate type conversion code.

Also, chrono::NaiveDate violates these rules. It is serialized as TEXT for SQLite.

/// The date SQL type.
///
/// ### [`ToSql`](../serialize/trait.ToSql.html) impls
///
/// - [`chrono::NaiveDate`][NaiveDate] with `feature = "chrono"`
///
/// ### [`FromSql`](../deserialize/trait.FromSql.html) impls
///
/// - [`chrono::NaiveDate`][NaiveDate] with `feature = "chrono"`
///
/// [NaiveDate]: /chrono/naive/date/struct.NaiveDate.html
#[derive(Debug, Clone, Copy, Default, QueryId, SqlType)]
#[postgres(oid = "1082", array_oid = "1182")]
#[sqlite_type = "Text"]
#[mysql_type = "Date"]
pub struct Date;

@sameer
Copy link

sameer commented Aug 16, 2018

Either it should be implemented for both or none. It's misleading to implement it for NaiveDate and not Uuid since both can technically be serialized.

If it shouldn't be, is there some way this could be provided in a standardized manner? It is odd for users to have to manually implement ToSql and FromSql for types that can be serialized in general.

@weiznich
Copy link
Member

In my opinion NaiveDate (and similar types) falls in the same category like booleans. They are fundamental in such a way that it is not possible to write larger application without using them. So not supporting the is not an option.
On the other hand it is possible to write large application without using uuid's, because for nearly all use cases using a 64 bit identifier should be enough.

@sgrif
Copy link
Member

sgrif commented Aug 16, 2018

We also support it because SQLite does have a ton of built-in functions for dealing with dates. Yes, it represents them as strings, but that's ultimately an implementation detail, not something relevant to whether the type exists or not. Importantly, there is a canonical way to represent a date in SQLite.

That is not the true of UUIDs. We could store the raw bytes, or we could store the text representation. It's not obvious which we should choose, and we would be incompatible with existing databases when we choose one or the other.

Ultimately when there's a clear representation of a type and semantically it exists for a given backend, we support it even if there's some mismatch. For example, even PG's datetime type supports a different range of dates than chrono. Given that SQLite is fully dynamically typed, if we really wanted to only support what strictly could be represented, the only type that we could support for SQLite is Vec<u8>. We do have to draw the line somewhere though, and this is where we've chosen to draw it for the time being.

@MightyPork
Copy link

MightyPork commented Mar 11, 2021

Here is an updated version of the code shared by @0xcaff , as I couldn't get the original to work anymore.
Really wish this was part of the crate, so we wouldn't have to use the wrapper. But, it works.

The "uuid" feature maybe should be called "postgres-uuid", took me a while to figure out it isn't implemented for sqlite. The same goes for most of the other features, like serde_json.

use uuid;
use std::io::prelude::*;
use diesel::deserialize::{self, FromSql};
use diesel::serialize::{self, IsNull, Output, ToSql};
use diesel::sql_types::{Binary};
use diesel::sqlite::Sqlite;
use diesel::backend::Backend;
use std::fmt::{Display, Formatter};
use std::fmt;

#[derive(Debug, Clone, Copy, FromSqlRow, AsExpression, Hash, Eq, PartialEq)]
#[sql_type = "Binary"]
pub struct UUID(pub uuid::Uuid);

impl UUID {
    pub fn random() -> Self {
        Self(uuid::Uuid::new_v4())
    }
}

impl From<UUID> for uuid::Uuid {
    fn from(s: UUID) -> Self {
        s.0
    }
}

impl Display for UUID {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromSql<Binary, Sqlite> for UUID {
    fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
        let bytes = not_none!(bytes);
        uuid::Uuid::from_slice(bytes.read_blob()).map(UUID).map_err(|e| e.into())
    }
}

impl ToSql<Binary, Sqlite> for UUID {
    fn to_sql<W: Write>(&self, out: &mut Output<W, Sqlite>) -> serialize::Result {
        out.write_all(self.0.as_bytes())
            .map(|_| IsNull::No)
            .map_err(Into::into)
    }
}

@GTB3NW
Copy link

GTB3NW commented Apr 12, 2021

+1 for rename of the feature. I've spent about an hour looking into this only to find out the type is only compat with PG. Using rusqlite allows for UUID usage as BLOB format, it would be really nice to see, but if it violates your standards then :(

WhiteHatTux added a commit to WhiteHatTux/todo-in-rust that referenced this issue May 10, 2021
This was a little bit more complicated than expected.
I had to get rid of the uuid, because sqlite doesn't support it and
diesel refuses to include a converter for that. :-(
diesel-rs/diesel#364

adding my own converter will come later. until then I just convert from
uuid -> string -> uuid by myself.
@w4
Copy link

w4 commented Sep 18, 2021

Following on from @MightyPork's code, you can make the backend generic for all database types supporting the Binary type:

impl<B: diesel::backend::Backend> diesel::deserialize::FromSql<Binary, B> for Uuid
where
    Vec<u8>: diesel::deserialize::FromSql<Binary, B>,
{
    fn from_sql(bytes: Option<&B::RawValue>) -> diesel::deserialize::Result<Self> {
        let value = <Vec<u8>>::from_sql(bytes)?;
        uuid::Uuid::from_slice(&value)
            .map(Uuid)
            .map_err(|e| e.into())
    }
}

impl<B: diesel::backend::Backend> diesel::serialize::ToSql<Binary, B> for Uuid
where
    [u8]: diesel::serialize::ToSql<Binary, B>,
{
    fn to_sql<W: Write>(
        &self,
        out: &mut diesel::serialize::Output<W, B>,
    ) -> diesel::serialize::Result {
        out.write_all(self.0.as_bytes())
            .map(|_| diesel::serialize::IsNull::No)
            .map_err(Into::into)
    }
}

@teschmitt
Copy link

Based on w4's and MightyPork's solutions (which seem to require diesel v1.x), I adapted their UUID module for diesel v2.1.5:

use diesel::{
    backend::Backend,
    deserialize::{self, FromSql},
    serialize::{self, Output, ToSql},
    sql_types::Binary,
    AsExpression, FromSqlRow,
};
use std::fmt::{self, Display, Formatter};
use uuid;

#[derive(Debug, Default, Clone, Copy, FromSqlRow, AsExpression, Hash, Eq, PartialEq)]
#[diesel(sql_type = Binary)]
pub struct UUID(pub uuid::Uuid);

impl UUID {
    pub fn new_v4() -> Self {
        Self(uuid::Uuid::new_v4())
    }
}

impl From<UUID> for uuid::Uuid {
    fn from(s: UUID) -> Self {
        s.0
    }
}

impl From<uuid::Uuid> for UUID {
    fn from(s: uuid::Uuid) -> Self {
        UUID(s)
    }
}

impl Display for UUID {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<B: Backend> FromSql<Binary, B> for UUID
where
    Vec<u8>: FromSql<Binary, B>,
{
    fn from_sql(bytes: <B as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let value = <Vec<u8>>::from_sql(bytes)?;
        uuid::Uuid::from_slice(&value)
            .map(UUID)
            .map_err(|e| e.into())
    }
}

impl<B: Backend> ToSql<Binary, B> for UUID
where
    [u8]: ToSql<Binary, B>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, B>) -> serialize::Result {
        self.0.as_bytes().to_sql(out)
    }
}

This has worked for me in a small SQLite-based project.

@adam-53-r
Copy link

Based on w4's and MightyPork's solutions (which seem to require diesel v1.x), I adapted their UUID module for diesel v2.1.5:

use diesel::{
    backend::Backend,
    deserialize::{self, FromSql},
    serialize::{self, Output, ToSql},
    sql_types::Binary,
    AsExpression, FromSqlRow,
};
use std::fmt::{self, Display, Formatter};
use uuid;

#[derive(Debug, Default, Clone, Copy, FromSqlRow, AsExpression, Hash, Eq, PartialEq)]
#[diesel(sql_type = Binary)]
pub struct UUID(pub uuid::Uuid);

impl UUID {
    pub fn new_v4() -> Self {
        Self(uuid::Uuid::new_v4())
    }
}

impl From<UUID> for uuid::Uuid {
    fn from(s: UUID) -> Self {
        s.0
    }
}

impl From<uuid::Uuid> for UUID {
    fn from(s: uuid::Uuid) -> Self {
        UUID(s)
    }
}

impl Display for UUID {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<B: Backend> FromSql<Binary, B> for UUID
where
    Vec<u8>: FromSql<Binary, B>,
{
    fn from_sql(bytes: <B as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let value = <Vec<u8>>::from_sql(bytes)?;
        uuid::Uuid::from_slice(&value)
            .map(UUID)
            .map_err(|e| e.into())
    }
}

impl<B: Backend> ToSql<Binary, B> for UUID
where
    [u8]: ToSql<Binary, B>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, B>) -> serialize::Result {
        self.0.as_bytes().to_sql(out)
    }
}

This has worked for me in a small SQLite-based project.

Could it be possible to save it in Text instead of Binary?

@teschmitt
Copy link

teschmitt commented Mar 28, 2024

Could it be possible to save it in Text instead of Binary?

Saving it as text is the default behavior which you get when you declare the field as uuid_text in the migration:

create table posts
(
    id uuid_text not null primary key,
    text text not null,
);

Then you just need to remember to always convert to and from String when writing and reading from the DB. If you want to map it transparently, I suppose you could do it analog to the implementation above:

Replace Binary with Text and fiddle around with the to_sql() and from_sql() methods:

// ...

#[diesel(sql_type = Text)]
pub struct UUID(pub uuid::Uuid);

// ...

impl<B: Backend> FromSql<Text, B> for UUID
where
    String: FromSql<Text, B>,
{
    fn from_sql(bytes: <B as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let value = String::from_sql(bytes)?;
        uuid!(value).map(UUID).map_err(|e| e.into())
    }
}

impl<B: Backend> ToSql<Binary, B> for UUID
where
    str: ToSql<Text, B>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, B>) -> serialize::Result {
        self.0.to_string().to_sql(out)
    }
}

I haven't tested this so you might have to put some more work into the methods, but that would be the rough idea.

@adam-53-r
Copy link

impl<B: Backend> ToSql<Binary, B> for UUID
where
    str: ToSql<Text, B>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, B>) -> serialize::Result {
        self.0.to_string().to_sql(out)
    }
}

It would'nt let me do this because of str needing lifetime annotations or something like that (sorry I'm pretty new).

Eventually I saw this in the documentation and ended up with this implementation that works fine:

// ...

impl<B: Backend> FromSql<Text, B> for UUID
where
    String: FromSql<Text, B>,
{
    fn from_sql(bytes: <B as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let value = String::from_sql(bytes)?;
        uuid::Uuid::from_str(&value.as_str())
            .map(UUID)
            .map_err(|e| e.into())
    }
}

impl ToSql<Text, Sqlite> for UUID
where
    String: ToSql<Text, Sqlite>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        out.set_value(self.0.as_hyphenated().to_string());
        Ok(IsNull::No)
    }
}

Thank you very much for your help @teschmitt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests