Atmosphere

Atmosphere is a lightweight SQL framework designed for sustainable, database-reliant systems. It leverages Rust's powerful type and macro systems to derive SQL schemas from your rust struct definitions into an advanced trait system.

It works by leveraging the sqlx crate and the Rust macro system to allow you to work easily with relational-database mapped entities, while still enabling low level usage of the underlying sqlx concepts.

Sections

Getting Started

Traits

Getting Started

To get started with atmosphere, install it and set up your first package. Next, you will want to define your schema.

Installation

Install Atmosphere

The easiest way to get started with atmosphere is to add the current stable release from crates.io as your dependency:

cargo add atmosphere

Define your Schema

To make use of Atmosphere, you must define your schema in a way that Atmosphere can understand it. To do so, you use Rust structs augmented with the Schema derive macro and some metadata which tells it how to map it to SQL.

Here is an example of what such a schema might look like if you are storing users and posts in a database.

extern crate atmosphere;
extern crate sqlx;
use atmosphere::prelude::*;

#[derive(Schema)]
#[table(schema = "public", name = "user")]
struct User {
    #[sql(pk)]
    id: i32,
    name: String,
    #[sql(unique)]
    email: String,
}

#[derive(Schema)]
#[table(schema = "public", name = "post")]
struct Post {
    #[sql(pk)]
    id: i32,
    #[sql(fk -> User, rename = "author_id")]
    author: i32,
    #[sql(unique)]
    title: String,
}
fn main() {
}

Table properties

Every type you annotate like this corresponds to one table in your Postgres database. You must set the table and schema name of the entities by setting the appropriate keys on the #[table] annotation.

extern crate atmosphere;
extern crate sqlx;
use atmosphere::prelude::*;
#[derive(Schema)]
#[table(schema = "public", name = "users")]
struct User {
    #[sql(pk)]
    id: i32,
    // ...
}
fn main() {
}

Column properties

Every struct member corresponds to one row of your backing table. Here you can use the #[sql] annotation to add metadata.

Queries

When using Atmosphere, you have two options for writing queries. Once you derive Schema on your entities, it gives you the ability to use querying traits that Atmosphere comes with. However, you can at any point reach down and write your queries in raw SQL, the way you would if you used sqlx directly.

Using Atmosphere Traits

Given the Schema from the section before, here is some examples that show how Atmosphere creates traits that allow for simple operations on the tables.

extern crate atmosphere;
extern crate sqlx;
extern crate tokio;
use atmosphere::prelude::*;
#[derive(Schema, Debug, PartialEq)]
#[table(schema = "public", name = "user")]
struct User {
    #[sql(pk)]
    id: i32,
    name: String,
    #[sql(unique)]
    email: String,
}
#[derive(Schema, Debug, PartialEq)]
#[table(schema = "public", name = "post")]
struct Post {
    #[sql(pk)]
    id: i32,
    #[sql(fk -> User, rename = "author_id")]
    author: i32,
    #[sql(unique)]
    title: String,
}
async fn test() -> std::result::Result<(), Box<dyn std::error::Error>> {
let database = std::env::var("DATABASE_URL").unwrap();
let pool = atmosphere::Pool::connect(&database).await?;

let mut user = User {
    id: 0,
    name: "demo".to_owned(),
    email: "some@email.com".to_owned(),
};

user.save(&pool).await?;
user.delete(&pool).await?;
user.create(&pool).await?;

assert_eq!(
    User::read(&pool, &0).await?,
    User::find_by_email(&pool, &"some@email.com".to_string()).await?.unwrap()
);

let mut post = Post {
    id: 0,
    author: 0,
    title: "test".to_owned()
};

post.save(&pool).await?;

Post::find_by_author(&pool, &0).await?;

// Inter-Table Operations

Post { id: 1, author: 0, title: "test1".to_owned() }
    .author(&pool).await?;

user.posts(&pool).await?;
user.delete_posts(&pool).await?;
Ok(())
}
fn main() {}

Using raw SQL

As previously explained, it is always possible to reach down and perform raw SQL queries on an Atmosphere pool, since it is just an alias for an sqlx one.

extern crate atmosphere;
extern crate sqlx;
extern crate tokio;
use atmosphere::prelude::*;
async fn test() -> std::result::Result<(), Box<dyn std::error::Error>> {
let database = std::env::var("DATABASE_URL").unwrap();
let pool = atmosphere::Pool::connect(&database).await?;

sqlx::query("DROP TABLE foo;")
    .execute(&pool).await?;
Ok(())
}
fn main() {}

Traits

Given that you have derived Schema for your entities, Atmosphere provides you with some traits for simple CRUD operations on your database. These make it very straightforward to do simple operations, but keep in mind that you are always able to reach down and write raw SQL manually where it makes sense.

Here are the traits that Atmosphere exposes, along with some examples:

Create

The Create trait allows you to create new rows in your tables. Here is an example of how to create a user, given that you have derived its Schema:

extern crate atmosphere;
extern crate sqlx;
extern crate tokio;
use atmosphere::prelude::*;
#[derive(Schema, Debug, PartialEq)]
#[table(schema = "public", name = "user")]
struct User {
    #[sql(pk)]
    id: i32,
    name: String,
    #[sql(unique)]
    email: String,
}

async fn test() -> std::result::Result<(), Box<dyn std::error::Error>> {
let database = std::env::var("DATABASE_URL").unwrap();
let pool = atmosphere::Pool::connect(&database).await?;

let mut user = User {
    id: 0,
    name: "demo".to_owned(),
    email: "some@email.com".to_owned(),
};

user.create(&pool).await?;
Ok(())
}
fn main() {}

Read

The Read trait allows you to read entities from rows in your table. Here is an example of how to create a user, given that you have derived its Schema:

extern crate atmosphere;
extern crate sqlx;
extern crate tokio;
use atmosphere::prelude::*;
#[derive(Schema, Debug, PartialEq)]
#[table(schema = "public", name = "user")]
struct User {
    #[sql(pk)]
    id: i32,
    name: String,
    #[sql(unique)]
    email: String,
}

async fn test() -> std::result::Result<(), Box<dyn std::error::Error>> {
let database = std::env::var("DATABASE_URL").unwrap();
let pool = atmosphere::Pool::connect(&database).await?;

// fetch all users
let users = User::find_all(&pool).await?;

// find user by primary key
let mut user = User::find(&pool, &0).await?;

// refresh user data
user.reload(&pool).await?;
Ok(())
}
fn main() {}

Update

The Update trait allows you to read entities from rows in your table. Here is an example of how to create a user, given that you have derived its Schema:

extern crate atmosphere;
extern crate sqlx;
extern crate tokio;
use atmosphere::prelude::*;
#[derive(Schema, Debug, PartialEq)]
#[table(schema = "public", name = "user")]
struct User {
    #[sql(pk)]
    id: i32,
    name: String,
    #[sql(unique)]
    email: String,
}

async fn test() -> std::result::Result<(), Box<dyn std::error::Error>> {
let database = std::env::var("DATABASE_URL").unwrap();
let pool = atmosphere::Pool::connect(&database).await?;

// find user by primary key
let mut user = User::find(&pool, &0).await?;

user.email = "joe@example.com".into();

// update user data
user.update(&pool).await?;
Ok(())
}
fn main() {}

Delete

The Delete trait allows you to read entities from rows in your table. Here is an example of how to create a user, given that you have derived its Schema:

extern crate atmosphere;
extern crate sqlx;
extern crate tokio;
use atmosphere::prelude::*;
#[derive(Schema, Debug, PartialEq)]
#[table(schema = "public", name = "user")]
struct User {
    #[sql(pk)]
    id: i32,
    name: String,
    #[sql(unique)]
    email: String,
}

async fn test() -> std::result::Result<(), Box<dyn std::error::Error>> {
let database = std::env::var("DATABASE_URL").unwrap();
let pool = atmosphere::Pool::connect(&database).await?;

// find user by primary key
let mut user = User::find(&pool, &0).await?;

// delete user data
user.delete(&pool).await?;

// delete by primary key
User::delete_by(&pool, &4).await?;
Ok(())
}
fn main() {}