Skip to content

Levosia

Published: at 10:22 AM

Github: Levosia.

Imagine you’re building an application in Rust and need to talk to the database normally you’d use sqlx and it would look like this:

#[derive(Debug, sqlx::FromRow)]
struct User {
    id: i32,
    name: String,
    email: String,
}

async fn create_user(pool: &PgPool, name: &str, email: &str) -> Result<User, sqlx::Error> {
    let user = sqlx::query_as::<_, User>(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
    )
    .bind(name)
    .bind(email)
    .fetch_one(pool)
    .await?;

    Ok(user)
}

Works great. Well.. now I want to add a delete method

sync fn delete_user(pool: &sqlx::PgPool, user_id: i32) -> sqlx::Result<()> {
    sqlx::query("DELETE FROM users WHERE id = $1")
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

… and I want to update the email

async fn update_user_email(pool: &sqlx::PgPool, user_id: i32, new_email: &str) -> sqlx::Result<()> {
    sqlx::query("UPDATE users SET email = $1 WHERE id = $2")
        .bind(new_email)
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

In my experience writing methods like this becomes repetitive and anoying. I like Rust and I was bored so I made Levosia. This is a light query and method builder for sqlx, currently only supporting postgres. It leverages proc macros to build methods at compile time.

Now your code looks like this:

#[levosia]
#[derive(Debug, sqlx::FromRow)]
struct User {
    id: Autogenerated<i32>,
    name: String,
    email: String,
}

let user = User::create(String::from("Hello"), String::from("world")).await.unwrap();

user.update_email(String::from("Goodbye")).await.unwrap();

user.delete().await.unwrap();

You can make relations like this:

#[leviosa]
#[derive(Debug, FromRow, Clone)]
struct User {
    id: AutoGenerated<i32>,
    name: String,
    created_at: AutoGenerated<DateTime<Utc>>, 
}

#[leviosa]
#[derive(Debug, FromRow, Clone)]
struct Post {
    id: AutoGenerated<i32>,
    my_data: String,
    user: Relation<User>, // Custom type that the macro uses to build lazy loading methods
}
// Notice how the method ignores Autogenerated fields. 
// If you want to explicitly set null use an Option in your struct.
let user = User::create(&db, String::from("bob")) 
        .await
        .expect("Could not create");

let relation = Relation::new(user.id.0); 

let post = Post::create(&db, String::from("My post data"), relation)
        .await
        .expect("Could not create");

// lazy load realation
let post_user = post
        .clone() // needs to take ownership of self (for now I don't like this)
        .load_user(&db) // method automatically added to Post in compile time. 
        .await
        .expect("Failed to execute");

// there is no back relations.. however you can use the find query builder. 
// WARNING inputs not santized (yet) 
let posts = Posts::find()
    .select(&format!("user = {}", post_user.id.0))
    .execute(&db)
    .await
    .expect("Failed where Clause");

I started this project wanting to learn more about procedural macros and build somthing cool. This is a proof of concept not a production ready tool. It’s missing many features, the query bulders do not sanitize input so please be carful if you want to experiment with it.