Search code examples
postgresqlrustrust-diesel

How do I select a subset of columns with diesel-rs?


I am struggling for several hours now to query a subset of the available columns of a table as well as including a calculation in it. I know that it is not the best way to execute the calculation in the select query, but for now, I am just working on a prototype and it should be feasible for that.

I am using diesel-rs as a ORM for all my database actions in my back-end implementation. The data will be stored in a PostgresSQL server. The full table - as stored in the database - is created using the following query:

CREATE TABLE airports
(
    id          SERIAL PRIMARY KEY,
    icao_code   VARCHAR(4) NOT NULL UNIQUE, -- the official ICAO code of the airport
    last_update TIMESTAMP  NOT NULL,        -- when were the information updated the last time?
    country     VARCHAR(2) NOT NULL,        -- two letter country code
    longitude   REAL       NOT NULL,        -- with 6 decimal places
    latitude    REAL       NOT NULL,        -- with 6 decimal places
    name        VARCHAR    NOT NULL         -- just a human readable name of the airport
);

Running diesel migrations run generates the airports table definition and querying the database works without any issue.

Now I am trying to query a list of all airports (their ICAO code) with the corresponding coordinates as well as the distance to a supplied coordinate. Therefore, I created the following diesel-rs table! macro myself

table! {
    airport_by_distance (icao_code) {
        icao_code -> Varchar,
        longitude -> Float8,
        latitude -> Float8,
        distance -> Float8,
    }
}

as well as the struct corresponding to the diesel-rs definition:

#[derive(QueryableByName)]
#[table_name = "airport_by_distance"]
struct AirportByDistance {
    icao_code: String,
    longitude: f64,
    latitude: f64,
    distance: f64,
}

The following snipped - as of my understanding - should query the required information:

use diesel::dsl::sql_query;

let latitude = 4.000001;
let longitude = 47.000001;

let query_sql = format!("SELECT icao_code, longitude, latitude, (3959.0 * acos(cos(radians({lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians({long})) + sin(radians({lat})) * sin(radians(latitude)))) AS distance FROM airports ORDER BY distance;", lat=latitude, long=longitude);
let result = match sql_query(query_sql).load::<AirportByDistance>(database_connection) {
    Ok(result) => result,
    Err(error) => {
        error!("{:?}", error);
        return Err(());
    }
};

Unfortunately, executing the load method, results in a DeserializationError(Custom { kind: UnexpectedEof, error: "failed to fill whole buffer" }) error.

The query which was executed was:

SELECT icao_code,
       longitude,
       latitude,
       (3959.0 * acos(cos(radians(4.000001)) * cos(radians(latitude)) * cos(radians(longitude) - radians(47.000001)) +
                      sin(radians(4.000001)) * sin(radians(latitude)))) AS distance
FROM airports
ORDER BY distance;

I took it and executed it manually, but it worked flawlessly. I even tried removing the calculation and just selecting a subset of columns, but with no luck either.

Right now I am not sure what I am doing wrong. How can I fix this issue?


EDIT with the fixed code: For the ones who are interested how to code would look like after using the helpful advice of Rasmus:

The table! macro is completely gone and the data definition struct looks like this:

#[derive(Queryable)]
struct AirportByDistance {
    icao_code: String,
    longitude: f32,
    latitude: f32,
    distance: f64,
}

The code for the querying the database is as follows:

let result = match airports.select(
    (
        icao_code,
        longitude,
        latitude,
        sql::<Double>(
            &format!("(3959.0 * acos(cos(radians({lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians({long})) + sin(radians({lat})) * sin(radians(latitude)))) AS distance", lat=latitude_reference, long=longitude_reference)
        )
    )
).load::<AirportByDistance>(database_connection)
{
    Ok(result) => result,
    Err(error) => {
        error!("{:?}", error);
        return Err(());
    }
};

for airport in result {
    info!(
        "AIRPORT: {} has {}nm distance",
        airport.icao_code, airport.distance
    );
}

Solution

  • I think the problem is that the deserializer don't know the raw types of the queried columns.

    Try to use typed diesel names/values as much as possible, and explicit sql strings only where needed. And I don't think the "fake" table declaration airports_by_distance helps. Maybe something like this:

    use diesel::sql_types::Double;
    
    let result = a::airports
        .select((
            a::icao_code,
            a::longitude,
            a::latitude,
            sql::<Double>(&format!("(3959.0 * acos(cos(radians({lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians({long})) + sin(radians({lat})) * sin(radians(latitude)))) AS distance", lat=latitude, long=longitue)
        ))
        .load::<AirportByDistance>(&db)?
    

    (Using the table! macro manually is basically just telling diesel that such a table will exist in the actual database when running the program. If that is not true, you will get runtime errors.)