Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
415 views
in Technique[技术] by (71.8m points)

generics - How do I combine multiple functions using Diesel into one through abstraction?

I have the following two functions:

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, Error> {
    let res = types::ethereum::table
        .order(types::ethereum::time.desc())
        .limit(1)
        .load::<types::ETHRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format_err!("Error here! {:?}", err)),
    }
}

pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, Error> {
    let res = types::bitcoin::table
        .order(types::bitcoin::time.desc())
        .limit(1)
        .load::<types::BTCRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format_err!("Error here! {:?}", err)),
    }
}

I want to combine both into one function. I have tried out a few different ways, but

  1. I am quite new to Rust
  2. Diesel has weird types (or at least that's what it feels like)

What are some ways to merge these two functions (which differ in only the fields types::ethereum and ETHRecord into one unified function get_most_recent_entry?

These are my database struct definitions (the SQL schemas are equivalently defined):

#[derive(Insertable, Queryable, Debug)]
#[table_name="bitcoin"]
pub struct BTCRecord {
    pub time: i32,
    pub market_cap: f32,
    pub price_btc: f32,
    pub price_usd: f32,
    pub vol_usd: f32,
}

and the type of

`types::ethereum::time` is `database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::columns::time`

and the type of

`types::ethereum::table` is
`database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::table`
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

First, let's start with a MCVE. This is a tool that professional programmers use when trying to understand a problem. It removes extraneous detail but provides enough detail for anyone to be able to pick it up and reproduce the situation. Compare how much code is present here that you didn't provide. Each missing piece is something that an answerer has to guess on as well as your time and their time generating.

[dependencies]
diesel = { version = "1.0.0-beta", features = ["sqlite"] }
#[macro_use]
extern crate diesel;

use diesel::prelude::*;
use diesel::SqliteConnection;

mod types {
    table! {
        bitcoin (time) {
            time -> Int4,
        }
    }

    table! {
        ethereum (time) {
            time -> Int4,
        }
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name="bitcoin"]
    pub struct BtcRecord {
        pub time: i32,
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name="ethereum"]
    pub struct EthRecord {
        pub time: i32,
    }
}

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
    let res = types::ethereum::table
        .order(types::ethereum::time.desc())
        .limit(1)
        .load::<types::EthRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format!("Error here! {:?}", err)),
    }
}

pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, String> {
    let res = types::bitcoin::table
        .order(types::bitcoin::time.desc())
        .limit(1)
        .load::<types::BtcRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format!("Error here! {:?}", err)),
    }
}

Next, perform a diff between the two pieces of code to identify the differences. You stated:

which differ in only the fields types::ethereum and ETHRecord

However, they differ in four locations. Just because something has the same prefix doesn't mean you can pass that prefix around. Modules aren't concepts that exist at runtime in Rust:

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
    // ^^^^^^^^^^^^^^^^^^^^^^^^^
    let res = types::ethereum::table
    //               ^^^^^^^^ 
        .order(types::ethereum::time.desc())
    //                ^^^^^^^^ 
        .limit(1)
        .load::<types::EthRecord>(&*conn);
    //                 ^^^^^^^^^

Let's copy and paste one of the functions and replace all the unique values with dummies:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String> {
    let res = table
        .order(time.desc())
        .limit(1)
        .load::<Record>(&*conn);
    // ...

This next part isn't pretty. Basically, the compiler will tell you every trait bound that isn't met, one-by-one. You "just" copy each error back to the code to set up all the constraints:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    <Tbl as OrderDsl<Desc<Expr>>>::Output: LimitDsl,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: RunQueryDsl<SqliteConnection> + Query,
    Sqlite: HasSqlType<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryFragment<Sqlite>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryId,
    Record: Queryable<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType, Sqlite>,

This leads to the new error:

error[E0609]: no field `time` on type `&Record`
  --> src/main.rs:64:38
   |
64 |                 Ok(x.get(0).unwrap().time)
   |                                      ^^^^

You cannot assume any fields on a generic type, we need a trait:

pub trait Time {
    fn time(&self) -> i32;
}

You:

  • implement the trait for both concrete types
  • add this trait bound to Record
  • call .time() in the method

All together:

#[macro_use]
extern crate diesel;

use diesel::prelude::*;
use diesel::SqliteConnection;

mod types {
    table! {
        bitcoin (time) {
            time -> Int4,
        }
    }

    table! {
        ethereum (time) {
            time -> Int4,
        }
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name = "bitcoin"]
    pub struct BtcRecord {
        pub time: i32,
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name = "ethereum"]
    pub struct EthRecord {
        pub time: i32,
    }
}

pub trait Time {
    fn time(&self) -> i32;
}

impl Time for types::EthRecord {
    fn time(&self) -> i32 {
        self.time
    }
}

impl Time for types::BtcRecord {
    fn time(&self) -> i32 {
        self.time
    }
}

use diesel::sqlite::Sqlite;
use diesel::types::HasSqlType;
use diesel::query_dsl::methods::{LimitDsl, OrderDsl};
use diesel::expression::operators::Desc;
use diesel::query_builder::{Query, QueryFragment, QueryId};
use diesel::Queryable;

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    <Tbl as OrderDsl<Desc<Expr>>>::Output: LimitDsl,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: RunQueryDsl<SqliteConnection> + Query,
    Sqlite: HasSqlType<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryFragment<Sqlite>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryId,
    Record: Queryable<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType, Sqlite> + Time,
{
    let res = table.order(time.desc()).limit(1).load::<Record>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time())
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format!("Error here! {:?}", err)),
    }
}

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
    get_most_recent_entry::<_, _, types::EthRecord>(
        conn,
        types::ethereum::table,
        types::ethereum::time,
    )
}

pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, String> {
    get_most_recent_entry::<_, _, types::BtcRecord>(
        conn,
        types::bitcoin::table,
        types::bitcoin::time,
    )
}

The next steps require a deeper dive into Diesel. The helper_types module contains type aliases that allow us to shorten the bounds:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    Order<Tbl, Desc<Expr>>: LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: RunQueryDsl<SqliteConnection>
        + Query
        + QueryFragment<Sqlite>
        + QueryId,
    Sqlite: HasSqlType<<Limit<Order<Tbl, Desc<Expr>>> as Query>::SqlType>,
    Record: Queryable<<Limit<Order<Tbl, Desc<Expr>>> as Query>::SqlType, Sqlite> + Time,

There's also a trait that wraps up all the Query*-related subtraits: LoadQuery. Using that, we can reduce it down to:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    Order<Tbl, Desc<Expr>>: LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
    Record: Time,

You can then make use of Diesel's first function and Results combinators to shorten the entire function:

use diesel::expression::operators::Desc;
use diesel::helper_types::{Limit, Order};
use diesel::query_dsl::methods::{LimitDsl, OrderDsl};
use diesel::query_dsl::LoadQuery;

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    Order<Tbl, Desc<Expr>>: LoadQuery<SqliteConnection, Record> + LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
    Record: Time,
{
    table
        .order(time.desc())
        .first(conn)
        .optional()
        .map(|x| x.map_or(0, |x| x.time()))
        .map_err(|e| format!("Error here! {:?}", e))
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...