Getting started

This crate provides the #[ergol] macro. You can use it by adding

ergol = "0.1"

to your dependencies.

It allows to persist the data in a database. For example, you just have to write

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;

#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: Option<i32>,
}
}

and the #[ergol] macro will generate most of the code you will need. You'll then be able to run code like the following:

extern crate tokio;
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: Option<i32>,
}
#[tokio::main]
async fn main() -> Result<(), ergol::tokio_postgres::Error> {
    let (db, connection) = ergol::connect(
        "host=localhost user=ergol password=ergol dbname=ergol",
        ergol::tokio_postgres::NoTls,
    )
    .await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
// Drop the user table if it exists
User::drop_table().execute(&db).await.ok();

// Create the user table
User::create_table().execute(&db).await?;

// Create a user and save it into the database
let mut user: User = User::create("thomas", "pa$$w0rd", Some(28)).save(&db).await?;

// Change some of its fields
*user.age.as_mut().unwrap() += 1;

// Update the user in the database
user.save(&db).await?;

// Fetch a user by its username thanks to the unique attribute
let user: Option<User> = User::get_by_username("thomas", &db).await?;

// Select all users
let users: Vec<User> = User::select().execute(&db).await?;
Ok(())
}

Ergol macro

The #[ergol] macro transforms your struct in a table, and gives you methods to easily access it.

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;

#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: i32,
}
}

The #[id] attribute

In every table, a primary key is required. For the moment, ergol requires to have an id column, which is an i32 field named id.

The #[unique] attribute

If a field is marked with the #[unique] attribute, the macro will generate extra methods to fetch an element from this attribute. For example, with the #[unique] pub username: String attribute, we can now use the following:

extern crate tokio;
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: i32,
}
#[tokio::main]
async fn main() -> Result<(), ergol::tokio_postgres::Error> {
    let (db, connection) = ergol::connect(
        "host=localhost user=ergol password=ergol dbname=ergol",
        ergol::tokio_postgres::NoTls,
    )
    .await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
User::drop_table().execute(&db).await.ok();
User::create_table().execute(&db).await?;
let user: Option<User> = User::get_by_username("thomas", &db).await?;
Ok(())
}

Enums in ergol

To use an enum in an ergol managed struct, this enum needs to derive Debug and PgEnum. An ergol managed enum cannot have attributes.

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;

#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub role: Role,
}

#[derive(PgEnum, Debug)]
pub enum Role {
    Guest,
    Admin,
}
}

One-to-one and many-to-one relationships

Let's say you want a user to be able to have projects. You can use the #[many_to_one] attribute in order to do so. Let's take the following code as an example:

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;

#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: Option<i32>,
}

#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[many_to_one(projects)] pub owner: User,
}
}

Once you have defined this struct, many more functions become available:

extern crate tokio;
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: Option<i32>,
}
#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[many_to_one(projects)] pub owner: User,
}
#[tokio::main]
async fn main() -> Result<(), ergol::tokio_postgres::Error> {
    let (db, connection) = ergol::connect(
        "host=localhost user=ergol password=ergol dbname=ergol",
        ergol::tokio_postgres::NoTls,
    )
    .await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
// Drop the tables if they exist
Project::drop_table().execute(&db).await.ok();
User::drop_table().execute(&db).await.ok();

// Create the tables
User::create_table().execute(&db).await?;
Project::create_table().execute(&db).await?;

// Create two users and save them into the database
let thomas: User = User::create("thomas", "pa$$w0rd", 28).save(&db).await?;
User::create("nicolas", "pa$$w0rd", 28).save(&db).await?;

// Create some projects for the user
let project: Project = Project::create("My first project", &thomas).save(&db).await?;
Project::create("My second project", &thomas).save(&db).await?;

// You can easily find all projects from the user
let projects: Vec<Project> = thomas.projects(&db).await?;

// You can also find the owner of a project
let owner: User = projects[0].owner(&db).await?;
Ok(())
}

You can similarly have one-to-one relationship between a user and a project by using the #[one_to_one] attribute:

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: Option<i32>,
}
#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[one_to_one(project)] pub owner: User,
}
}

This will add the UNIQUE attribute in the database and make the project method only return an option:

extern crate tokio;
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: Option<i32>,
}
#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[one_to_one(project)] pub owner: User,
}
#[tokio::main]
async fn main() -> Result<(), ergol::tokio_postgres::Error> {
    let (db, connection) = ergol::connect(
        "host=localhost user=ergol password=ergol dbname=ergol2",
        ergol::tokio_postgres::NoTls,
    )
    .await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
Project::drop_table().execute(&db).await.ok();
User::drop_table().execute(&db).await.ok();
User::create_table().execute(&db).await?;
Project::create_table().execute(&db).await?;
let thomas: User = User::create("thomas", "pa$$w0rd", 28).save(&db).await?;
// You can easily find a user's project
let project: Option<Project> = thomas.project(&db).await?;
Ok(())
}

Note that that way, a project has exactly one owner, but a user can have no project.

Many-to-many relationships

Ergol also supports many-to-many relationships. In order to do so, you need to use the #[many_to_many] attribute:

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;

#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: i32,
}

#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[many_to_many(visible_projects)] pub authorized_users: User,
}
}

The same way, you will have plenty of functions that you will be able to use to manage your objects:

extern crate tokio;
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: i32,
}
#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[many_to_many(visible_projects)] pub authorized_users: User,
}
#[tokio::main]
async fn main() -> Result<(), ergol::tokio_postgres::Error> {
    let (db, connection) = ergol::connect(
        "host=localhost user=ergol password=ergol dbname=ergol",
        ergol::tokio_postgres::NoTls,
    )
    .await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
Project::drop_table().execute(&db).await.ok();
User::drop_table().execute(&db).await.ok();
User::create_table().execute(&db).await?;
Project::create_table().execute(&db).await?;
User::create("thomas", "pa$$w0rd", 28).save(&db).await?;
User::create("nicolas", "pa$$w0rd", 28).save(&db).await?;
// Find some users in the database
let thomas = User::get_by_username("thomas", &db).await?.unwrap();
let nicolas = User::get_by_username("nicolas", &db).await?.unwrap();

// Create a project
let first_project = Project::create("My first project").save(&db).await?;

// Thomas can access this project
first_project.add_authorized_user(&thomas, &db).await?;

// The other way round
nicolas.add_visible_project(&first_project, &db).await?;

// The second project can only be used by thomas
let second_project = Project::create("My second project").save(&db).await?;
thomas.add_visible_project(&second_project, &db).await?;

// The third project can only be used by nicolas.
let third_project = Project::create("My third project").save(&db).await?;
third_project.add_authorized_user(&nicolas, &db).await?;

// You can easily retrieve all projects available for a certain user
let projects: Vec<Project> = thomas.visible_projects(&db).await?;

// And you can easily retrieve all users that have access to a certain project
let users: Vec<User> = first_project.authorized_users(&db).await?;

// You can easily remove a user from a project
let _: bool = first_project.remove_authorized_user(&thomas, &db).await?;

// Or vice-versa
let _: bool = nicolas.remove_visible_project(&first_project, &db).await?;

// The remove functions return true if they successfully removed something.
Ok(())
}

Extra information in a many-to-many relationship

It is possible to insert some extra information in a many to many relationship. The following exemple gives roles for the users for projects.

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;

#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: i32,
}

#[derive(PgEnum, Debug)]
pub enum Role {
   Admin,
   Write,
   Read,
}

#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[many_to_many(projects, Role)] pub users: User,
}
}

With these structures, the signature of generated methods change to take a role as argument, and to return tuples of (User, Role) or (Project, Role).

extern crate tokio;
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] pub id: i32,
    #[unique] pub username: String,
    pub password: String,
    pub age: i32,
}
#[derive(PgEnum, Debug)]
pub enum Role {
   Admin,
   Write,
   Read,
}
#[ergol]
pub struct Project {
    #[id] pub id: i32,
    pub name: String,
    #[many_to_many(projects, Role)] pub users: User,
}
#[tokio::main]
async fn main() -> Result<(), ergol::tokio_postgres::Error> {
    let (db, connection) = ergol::connect(
        "host=localhost user=ergol password=ergol dbname=ergol2",
        ergol::tokio_postgres::NoTls,
    )
    .await?;
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
Project::drop_table().execute(&db).await.ok();
User::drop_table().execute(&db).await.ok();
Role::drop_type().execute(&db).await.ok();
Role::create_type().execute(&db).await?;
User::create_table().execute(&db).await?;
Project::create_table().execute(&db).await?;
User::create("thomas", "pa$$w0rd", 28).save(&db).await?;
User::create("nicolas", "pa$$w0rd", 28).save(&db).await?;
let thomas = User::get_by_username("thomas", &db).await?.unwrap();
let nicolas = User::get_by_username("nicolas", &db).await?.unwrap();
let project = Project::create("My first project").save(&db).await?;
project.add_user(&thomas, Role::Admin, &db).await?;
nicolas.add_project(&project, Role::Read, &db).await?;

for (user, role) in project.users(&db).await? {
    println!("{} has {:?} rights on project {:?}", user.username, role, project.name);
}

let project = Project::create("My second project").save(&db).await?;
project.add_user(&thomas, Role::Admin, &db).await?;

let project = Project::create("My third project").save(&db).await?;
project.add_user(&nicolas, Role::Admin, &db).await?;
project.add_user(&thomas, Role::Read, &db).await?;

for (project, role) in thomas.projects(&db).await? {
    println!("{} has {:?} rights on project {:?}", thomas.username, role, project.name);
}
Ok(())
}

Using ergol with rocket

You can integrate ergol with rocket!

Note: ergol supports only rocket 0.5, which is not published on crates.io yet. If you wish to use ergol with rocket, you must use ergol git version instead of the crates.io version, in order to be able to use the with-rocket feature. The dependency in your Cargo.toml will look like this:

ergol = { git = "https://github.com/polymny/ergol", rev = "v0.1.0", features = ["with-rocket"] }

There is a little bit of boilerplate, but once everything is set up, you can use ergol in your routes!

extern crate ergol;
#[macro_use]
extern crate rocket;

use rocket::fairing::AdHoc;
use rocket::request::{FromRequest, Outcome, Request};
use rocket::State;

use ergol::deadpool::managed::Object;
use ergol::prelude::*;

/// A wrapper for a database connection extrated from a pool.
pub struct Db(Object<ergol::pool::Manager>);

impl Db {
    /// Extracts a database from a pool.
    pub async fn from_pool(pool: ergol::Pool) -> Db {
        Db(pool.get().await.unwrap())
    }
}

// This allows to pass directly Db instead of Ergol to the ergol's functions.
impl std::ops::Deref for Db {
    type Target = Object<ergol::pool::Manager>;
    fn deref(&self) -> &Self::Target {
        &*&self.0
    }
}

// This allows to use Db in routes parameters.
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Db {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let pool = request.guard::<&State<ergol::Pool>>().await.unwrap();
        let db = pool.get().await.unwrap();
        Outcome::Success(Db(db))
    }
}

#[ergol]
pub struct Item {
    #[id]
    id: i32,
    name: String,
    count: i32,
}

#[get("/add-item/<name>/<count>")]
async fn add_item(name: String, count: i32, db: Db) -> String {
    Item::create(name, count).save(&db).await.unwrap();
    "Item added".into()
}

#[get("/")]
async fn list_items(db: Db) -> String {
    let items = Item::select()
        .execute(&db)
        .await
        .unwrap()
        .into_iter()
        .map(|x| format!("  - {}: {}", x.name, x.count))
        .collect::<Vec<_>>()
        .join("\n");

    format!("{}\n{}", "List of items:", items)
}

#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
    // Setup rocket with its database connections pool.
    let rocket = rocket::build()
        .attach(AdHoc::on_ignite("Database", |rocket| async move {
            let pool = ergol::pool("host=localhost user=ergol password=ergol", 32).unwrap();
            rocket.manage(pool)
        }))
        .mount("/", routes![list_items, add_item])
        .ignite()
        .await?;

    // Get the pool from rocket.
    let pool = rocket.state::<ergol::Pool>().unwrap();

    {
        // Reset the Db at startup (you may not want to do this, but it's cool for an example).
        let db = Db::from_pool(pool.clone()).await;
        Item::drop_table().execute(&db).await.ok();
        Item::create_table().execute(&db).await.unwrap();
    }

    // rocket.launch().await
    Ok(())
}

See the example for more details.

Migrations

Ergol comes with a (pretty simple for the moment) system for migrations.

In order to be able to manage your migrations, you need to install ergol cli:

cargo install ergol_cli

Saving migrations

When building your application, the ergol proc macro automatically generates json files for each marked structure. Those json files represent the state of the database, and are stored in the migrations/current directory.

You can freeze a certain state of your database by running

ergol save

It will copy the migrations/current directory to a migrations/0 directory (or migrations/n n being the number of migrations you already have). It will also add up.sql and down.sql to migrate from n - 1 to n and back.

Note: when adding new columns, you probably will need to specify default values by editing directly the migrations/n/up.sql.

For example, let's say I start an application with the following model:

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] id: i32,
    username: String,
    email: String,
}
}

The up.sql will look like this

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR NOT NULL,
    email VARCHAR NOT NULL
);

and the down.sql will look like this

DROP TABLE users CASCADE;

I build my application, then run ergol save, and have my migrations/0/{up.sql,down.sql} files.

Let's say I want to add a new attribute to my User struct for the age of the users.

#![allow(unused)]
fn main() {
extern crate ergol;
use ergol::prelude::*;
#[ergol]
pub struct User {
    #[id] id: i32,
    username: String,
    email: String,
    age: i32,
}
}

If I run ergol save again, I will have a migrations/1/up.sql that will look like this:

ALTER TABLE users ADD age INT NOT NULL DEFAULT /* TODO default value */;

You need to change this code to set the default value for the column.

Running ergol hint will show the code that migrates from the last migration to the current migration.

Running migrations

You can run all migrations by running ergol migrate. Ergol creates a special table that it uses to keep track of what migration the database is in, and will run all migrations between the current migration of the database to the last saved migrations.

Reset

The last useful command you can do with ergol is ergol reset. It deletes the whole database, and recreate it using only the last migration. It is particularly useful when developping an app when you want to reset your database.