sfackler / rust-postgres

Native PostgreSQL driver for the Rust programming language
Apache License 2.0
3.46k stars 439 forks source link

Cannot convert from &str to custom Postgres enum #1150

Closed dwight9339 closed 3 months ago

dwight9339 commented 3 months ago

Alright, I've been trying to figure out what I'm doing wrong for the better part of a day now and am still struggling. Here is my custom enum in Postgres:

CREATE TYPE layer_type AS ENUM ('sparse', 'dense');

And here is my struct declaration:

use diesel_derive_enum::DbEnum;
use serde::ser::{Serialize, Serializer};
use serde::de::{self, Deserialize, Deserializer, Visitor, Unexpected};
use std::fmt;
use tokio_postgres::types::{ToSql, FromSql};

#[derive(Debug, Clone, PartialEq, DbEnum)]
#[ExistingTypePath = "crate::schema::sql_types::LayerType"]
pub enum LayerType {
    Sparse,
    Dense,
}

#[derive(Debug, ToSql, FromSql)]
#[postgres(name = "layer_type", rename_all = "lowercase")]
pub enum PgLayerType {
    Sparse,
    Dense,
}

impl From<LayerType> for PgLayerType {
    fn from(layer_type: LayerType) -> PgLayerType {
        match layer_type {
            LayerType::Dense => Self::Dense,
            LayerType::Sparse => Self::Sparse
        }
    }
}

I know it looks funky but I'm using Diesel for DB ops that can run in sync and tokio_postgres for async ops. I had originally tried just adding ToSql and FromSql to the list of derives and the postgres macro to the the existing enum type which is used by Diesel but tried separating them and implementing From for conversion because I thought maybe the derive function from the diesel_derive_enum might be causing some issue when mixed with the postgres_types derives but separating them has not yielded any different results so far.

Here is the code where I'm attempting to insert a row containing the enum in question:

pub async fn create_dense<'a>(&self, new_item: NewLayer, tile_data: Vec<u8>, db_connection: &mut AsyncPgConnection<'a>) -> Result<Layer, DatabaseError> {
    match db_connection.transaction().await {
        Ok(transaction) => {
            let pg_layer_type: PgLayerType = new_item.type_.into();
            let row = transaction.query_one("
                    INSERT INTO layers (tenant_id, map_id, layer_schema_id, name, description, type)
                    VALUES ($1, $2, $3, $4, $5, $6)
                    Returning *;
                ", 
                &[
                    &new_item.tenant_id,
                    &new_item.map_id,
                    &new_item.layer_schema_id,
                    &new_item.name,
                    &new_item.description,
                    &pg_layer_type
                ]
            )
                .await?;

            let layer = Layer::try_from(row)?;
            let file_key = get_tile_storage_key(&new_item.map_id, &layer.id);
            let storage_service = StorageServiceFactory::from_environment().await?;

            match storage_service.save(&file_key, &tile_data).await {
                Ok(()) => {
                    transaction.commit().await?;
                    Ok(layer)
                },
                Err(err) => {
                    let _rollback_res = transaction.rollback().await;
                    Err(err.into())
                }
            }
        },
        Err(err) => Err(err.into())
    }
}

I'm still getting this error though:

error deserializing column 6: cannot convert between the Rust type `&str` and the Postgres type `layer_type`
sfackler commented 3 months ago

That runtime error implies that it's being passed a &str rather than a &PgLayerType value.

dwight9339 commented 3 months ago

Thanks for your response. I tried passing &PgLayerType::Dense directly and that still didn't work. I tried implementing ToSql myself but that didn't help either:

impl ToSql for PgLayerType {
    fn to_sql(&self, _ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
        match *self {
            Self::Sparse => out.extend_from_slice(b"sparse"),
            Self::Dense => out.extend_from_slice(b"dense")
        };

        Ok(IsNull::No)
    }

    fn accepts(ty: &Type) -> bool {
        ty.name() == "layer_type"
    }

    to_sql_checked!();
}

I built a statement out of the query string and printed out the types if that's at all helpful:

Statement types: [Uuid, Int4, Int4, Varchar, Text, Other(Other { name: "layer_type", oid: 19728, kind: Enum(["sparse", "dense"]), schema: "public" })]
sfackler commented 3 months ago

If it's still complaining about the type being &str then it's probably happening in a different query than the one you're editing.

dwight9339 commented 3 months ago

Ah, you're right. The error was happening in my Layer::try_from() method. Thanks again.