Search code examples
genericsrustpolymorphismtraitsrust-sqlx

sqlx: implementing Decode and Type<DB> for associated trait type


I am trying to create a library which gets Values for multiple Tags from an SQL Database. Depending on the TagType which can be Analog or String - I need to return the matching value type. I tried to achieve this using a associated type named ValueType in the Tag trait

This is what i have got until now:

use sqlx::Row;

pub trait Tag {
    type ValueType;

    fn name(&self) -> String;
    fn tagtype(&self) -> TagType;
}

pub enum TagType {
    Analog = 1,
    String = 3,
}

pub struct AnalogTag(String);
pub struct StringTag(String);

impl Tag for AnalogTag {
    type ValueType = f64;

    fn name(&self) -> String {
        self.0.to_string()
    }

    fn tagtype(&self) -> TagType {
        TagType::Analog
    }
}

impl Tag for StringTag {
    type ValueType = String;

    fn name(&self) -> String {
        self.0.to_string()
    }

    fn tagtype(&self) -> TagType {
        TagType::String
    }
}

pub struct Value<T> {
    pub val: T,
    pub quality: i8,
}

impl<T> Value<T> {
    fn new(val: T, quality: i8) -> Self {
        Self { val, quality }
    }
}

pub async fn get_actual_value<T: Tag>(
    db_pool: sqlx::MssqlPool,
    tag: T,
) -> Result<Value<T::ValueType>, sqlx::Error> {
    let table = match tag.tagtype() {
        TagType::Analog => "AnalogLive",
        TagType::String => "StringLive",
    };
    let result = sqlx::query("SELECT Value, Quality FROM @P1 WHERE Tagname = @P2")
        .bind(table)
        .bind(tag.name())
        .fetch_one(&db_pool)
        .await?;
    let val = result.get("Value");
    let quality: i8 = result.get("Quality");
    Ok(Value::new(val, quality))
}

Anyhow, this will not work. I need to implement sqlx::Decode and Type<Mssql> traits but don't know how this can be done for ValueType that is an associated type of the trait Tag

the trait sqlx::Decode<'_, Mssql> is not implemented for <T as Tag>::ValueType the trait Type<Mssql> is not implemented for <T as Tag>::ValueType

Any help would be appreciated!

EDIT Updated the code-example to a minimal reproducible example


Solution

  • You are running into the problem here where you want to convert a type only known at runtime (the TagType enum) into a type known at compile time (T::ValueType). This is not trivial and requires some trickery. You have to understand that the compiler has zero knowledge about what tag.tagtype() will return at compile time.

    Luckily, the result::get() function already has the hard work for that problem implemented in it.

    So with a little bit of extra trait restrictions for T::ValueType, you can get this to work:

    use sqlx::Row;
    
    pub trait Tag {
        type ValueType;
    
        fn name(&self) -> String;
        fn tagtype(&self) -> TagType;
    }
    
    pub enum TagType {
        Analog = 1,
        String = 3,
    }
    
    pub struct AnalogTag(String);
    pub struct StringTag(String);
    
    impl Tag for AnalogTag {
        type ValueType = f64;
    
        fn name(&self) -> String {
            self.0.to_string()
        }
    
        fn tagtype(&self) -> TagType {
            TagType::Analog
        }
    }
    
    impl Tag for StringTag {
        type ValueType = String;
    
        fn name(&self) -> String {
            self.0.to_string()
        }
    
        fn tagtype(&self) -> TagType {
            TagType::String
        }
    }
    
    pub struct Value<T> {
        pub val: T,
        pub quality: i8,
    }
    
    impl<T> Value<T> {
        fn new(val: T, quality: i8) -> Self {
            Self { val, quality }
        }
    }
    
    pub async fn get_actual_value<T>(
        db_pool: sqlx::MssqlPool,
        tag: T,
    ) -> Result<Value<T::ValueType>, sqlx::Error>
    where
        T: Tag,
        for<'a> T::ValueType: sqlx::Decode<'a, sqlx::Mssql> + sqlx::Type<sqlx::Mssql>,
    {
        let table = match tag.tagtype() {
            TagType::Analog => "AnalogLive",
            TagType::String => "StringLive",
        };
        let result = sqlx::query("SELECT Value, Quality FROM @P1 WHERE Tagname = @P2")
            .bind(table)
            .bind(tag.name())
            .fetch_one(&db_pool)
            .await?;
        let quality: i8 = result.get("Quality");
    
        let val: T::ValueType = result.get("Value");
        Ok(Value::new(val, quality))
    }