Introduction to Rust

Rust is a modern systems programming language focused on safety, speed, and concurrency. It was designed by Graydon Hoare at Mozilla Research, with contributions from others, and has gained a reputation for enabling developers to write safe and efficient code.

Why Rust?

Rust is designed to solve problems that are common in systems programming, such as memory safety, concurrency, and performance. Some of the key features of Rust include:

  • Memory Safety: Rust ensures memory safety without using a garbage collector. This is achieved through a system of ownership with rules that the compiler checks at compile time.
  • Concurrency: Rust's ownership model ensures that data races are avoided at compile time. This makes it easier to write concurrent programs.
  • Performance: Rust is as fast as C and C++ in many cases due to its focus on zero-cost abstractions.

Learning Rust

Here are some excellent resources to get started with Rust:

Official Rust Book

The Rust Programming Language Book is the official Rust book, often referred to as "the book". It is comprehensive and covers everything from basic syntax and concepts to advanced topics.

Rust by Example

Rust by Example is a collection of runnable examples that illustrate various Rust concepts and standard libraries. It's a hands-on way to learn the language by seeing how various pieces of code work.

Rustlings

Rustlings is a set of small exercises that help you get familiar with reading and writing Rust code. Each exercise is focused on a specific aspect of the language, making it a great way to practice and improve your Rust skills.

Official Rust Learn Page

The Official Rust Learn Page provides a curated list of resources, including books, courses, and tools that can help you on your journey to mastering Rust.

Installing Rust

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety. Below are the instructions to download and install Rust on both Linux/Unix and Windows systems.

Installation on Linux/Unix

Install Dependencies

Before installing Rust, ensure you have the necessary dependencies. In this guide I will show you the process of installing it in Debian-based Distros but it works similar in other Distributions.

For Debian-based distributions (like Ubuntu):

sudo apt update
sudo apt install build-essential curl

Install Rust, rustup and Cargo

The official Rust Website recommends downloading Rust with rustup which is a tool for managing Rust versions. To install Rust with rustup, run the following command in your terminal:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

This command will not only download Rust and rustup, but also Cargo which is Rust's package manager.

Verify the Installation

To verify that Rust is installed correctly, run:

rustc --version

Installation on Windows

Install the Rustup installer

I recommend downloading Rust on Windows from rustup.rs which provides an executable for Windows devices. Rustup

Update and Uninstall Rust

Rust is constantly being improved. To update to the latest version of Rust, use rustup:

rustup update

If you need to uninstall Rust for any reason, use rustup on the Unix Terminal or Windows Powershell/CMD:

rustup self uninstall

How to Download Rust Libraries with Cargo

Cargo is the Rust package manager, and it is used to manage Rust projects and their dependencies. This guide will walk you through the steps to download and use Rust libraries (also known as crates) with Cargo.

After installing, make sure Cargo is available by running:

cargo --version

Create a new Rust Project

To create a new Rust Project , use the following command.

cargo new my_project
cd my_project

This creates a new directory named my_project with a simple Rust project template.

Add Depenendencies

Open the Cargo.toml file in your project directory. This file manages the dependencies for your project. To add a dependency, specify the crate name and version under the [dependencies] section. For example, to add the rand crate (a popular library used to generate random numbers), add the following lines:

[dependencies]
rand = "1.0"

Update and Build Your Project

After adding dependencies to Cargo.toml, run the following command to download and compile them:

cargo build

Cargo will fetch the specified crates from the crates.io registry and compile them along with your project.

Use the Library in your Code

Once the library is downloaded and compiled, you can use it in your Rust code. Open src/main.rs and include the crate at the top of the file:

#![allow(unused)]
fn main() {
use rand;
}

Run your Project

To run your project, use the following command:

cargo run

Cargo will build and execute your project, including any code that utilizes the libraries you added.

Basics

Rust Syntax

1. Variables and Data Types

Scalar Types
#![allow(unused)]
fn main() {
let int_number: i32 = -42;          // signed integers: i8, i16, i32, i64, i128, isize
                                    // represent positive and negative numbers
                                    
let uint_number: u64 = 100_000;     // unsigned integers: u8, u16, u32, u64, u128, usize
                                    // represent only positive numbers

let float_number: f64 = 3.14;    // floating-point numbers: f32, f64
let is_rust_cool: bool = true;
let emoji: char = '😊'; 
}
Compound Types
#![allow(unused)]
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1); // tuple
let arr: [i32; 5] = [4, 5, 1, 2, 1]; // type: i32, length = 5
}
Standard Library Types
#![allow(unused)]
fn main() {
let borrowed_string = "Hello World"; // &str, a string slice

let mut owned_string = String::new(); // String
owned_string.push_str("Hello World");

let mut owned_string2 = String::from("Hello ");
owned_string2.push_str("World")

// difference: explained later with ownership

let values = vec![1, 2, 3]; // Vector (list)
let mut values2: Vec<i32> = Vec::new();

let mut numbers = vec![1, 2, 3];
}
User-Defined Types
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let person1 = Person {             // create an instance of the 'Person' struct and initialize all fields
        name: String::from("Alice"),
        age: 30,
    };
    println!("{} is {} old.", person1.name, person1.age);
}

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

2. Conditional Statements

#![allow(unused)]
fn main() {
let number = 0;

if number > 0 && number % 2 == 0 {
    println!("The number is positive and even.");
} else if number < 0 || number % 2 != 0 {
    println!("The number is negative or odd.");
} else {
    println!("The number is zero.");
}

match number {              // with match you have to handle all cases 
    1 => println!("One!"),
    2 | 3 | 5 | 7 => println!("This is a prime number."),
    4 | 6 | 8 | 9 | 10 => println!("This is a composite number."),
    _ => println!("The number is not between 1 and 10."),
}
}

3. Loops

#![allow(unused)]
fn main() {
for i in 1..6 {
    println!("Number: {}", i);
}

let mut x = 1;
while x <= 5 {
    println!("Number: {}", x);
    x += 1;
}

let mut count = 0;
loop {
    println!("Hello, world!");

    count += 1;
    if count >= 5 {
        break; // Exit the loop when count reaches 5
    }
}
}

4. Functions

fn add(a: i32, b: i32) -> i32 {     // returns an i32 integer
    a + b                           // don't need return keyword, automatically returned
}

fn main() {
    let result = add(5, 3);
    println!("The sum is: {}", result);
}

5. Exception Handling

// with type Result
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

// macros: a way of defining reusable chunks of code; they generate code
// panic! macro
fn main() {
    if 1 + 1 != 2 {
        panic!("Math is broken!");
    }
}

6. Ownership

Set of rules that govern how a Rust program manages memory.

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.
#![allow(unused)]
fn main() {
{                                                    // s is not valid here, it’s not yet declared
    let s = "hello";                                // s is valid from this point forward
    let mut owned_string = String::from("Hi");
}                      // this scope is now over, and s is no longer valid, rust calls `drop`


let s1 = String::from("Hello");
let s2 = s1;                        // s2 is now the owner
s1.push_str("Wont work");          // WRONG, won't work, because s1 has been "moved" to s2 -> deals with memory

}
My Image
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);                 // provides a reference to s1, reference borrowing

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}
// if wanting to modify the borrowed value
fn main() {
    let mut s = String::from("hello");

    change(&mut s);   // mutable
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
My Image
#![allow(unused)]
fn main() {
// reference borrowing: you can have either one mutable reference (to ensure thread safety) or any number of immutable references
//                      the reference must be valid meaning the borrowed variable has to exist

let mut s = String::from("hello");      
let r1 = &mut s;
let r2 = &mut s;                    // error

// This above won't work

// This will work
let mut s = String::from("hello");

{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;


// This won't work

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
// error -> because r3 wants to change data which r1 and r2 are reading 

// This will work
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);
}

Introduction to Rust and Database Integration

Overview

This page introduces the fundamental concepts of integrating Rust with databases. Learn about the importance of using Rust for database management and how it can enhance data manipulation and analysis.

Why Use Rust with Databases?

Rust is an excellent choice for database management due to its unique strengths:

  • Performance: Rust offers high performance similar to C and C++, making it ideal for handling large datasets and high-transaction environments.
  • Memory Safety: Rust's ownership system ensures memory safety without a garbage collector, reducing risks like memory leaks.
  • Concurrency: Rust prevents data races at compile time, enabling safe and efficient concurrent database operations.
  • Type Safety: Rust's strong type system catches many errors at compile time, reducing runtime issues and enhancing SQL query safety.
  • Robust Libraries: Libraries like diesel, sqlx, and rusqlite provide powerful, type-safe, and ergonomic APIs for interacting with various databases.

By using Rust, you can build fast, safe, and reliable database applications.

Setting Up Your Environment

Database Setup

Choose a database. For beginners, SQLite is recommended due to its simplicity and ease of setup.

Library Installation

Install necessary Rust libraries by adding them to your Cargo.toml file. Here’s an example for SQLite using rusqlite:

[dependencies]
rusqlite = "0.26"

For PostgreSQL using diesel:

[dependencies]
diesel = { version = "2.0", features = ["postgres"] }

For MySQL using sqlx:

[dependencies]
sqlx = { version = "0.6", features = ["mysql", "runtime-async-std-native-tls"] }

Code Example: Connecting to a Database

Using rusqlite with SQLite

extern crate rusqlite;
use rusqlite::{params, Connection, Result};

fn main() -> Result<()> {
    let conn = Connection::open("my_database.db")?;
    
    // Create the person table if it doesn't exist
    conn.execute(
        "CREATE TABLE IF NOT EXISTS person (
                  id              INTEGER PRIMARY KEY,
                  name            TEXT NOT NULL,
                  data            BLOB
                  )",
        [],
    )?;

    // Add a person to the table
    add_person(&conn, "John Doe", b"Some binary data")?;

    Ok(())
}

fn add_person(conn: &Connection, name: &str, data: &[u8]) -> Result<()> {
    conn.execute(
        "INSERT INTO person (name, data) VALUES (?1, ?2)",
        params![name, data],
    )?;
    Ok(())
}

Using diesel with PostgreSQL

#[macro_use]
extern crate diesel;
extern crate dotenv;

use diesel::prelude::*;
use dotenv::dotenv;
use std::env;

fn main() {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let connection = PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url));
}

Using sqlx with MySQL

use sqlx::mysql::MySqlPoolOptions;

#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = MySqlPoolOptions::new()
        .max_connections(5)
        .connect("mysql://user:password@localhost/database_name").await?;

    let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
        .fetch_one(&pool).await?;

    println!("Number of users: {}", row.0);

    Ok(())
}

Code for a webserver in Rust to connect it to a database i

use std::net::SocketAddr;
use std::sync::OnceLock;

use axum::extract::Path;
use axum::response::IntoResponse;
use axum::Router;
use axum::routing::get;
use sqlx::{Executor, query, SqlitePool};
use sqlx::sqlite::SqlitePoolOptions;
use tokio::net::TcpListener;

static DB_POOL: OnceLock<SqlitePool> = OnceLock::new();

async fn get_pool() -> &'static SqlitePool {
    if let Some(x) = DB_POOL.get() {
        return x;
    }

    let pool = SqlitePoolOptions::new()
        .max_connections(100)
        .connect("sqlite:mydb.sqlite")
        .await
        .expect("Failed to connect to the database!");
    DB_POOL.get_or_init(move || pool)
}


async fn fruit(Path(fruit_name): Path<String>) -> impl IntoResponse {
    let connection = get_pool().await;
    let found = sqlx::query!("select name, price from fruits where name = ?", fruit_name)
        .fetch_optional(connection).await;

    match found {
        Ok(record) => {
            match record {
                None => {
                    format!("No fruit exists by the name '{fruit_name}'")
                }
                Some(record) => {
                    format!("The fruit '{fruit_name}' currently has a price of {}", record.price)
                }
            }
        }
        Err(e) => {
            format!("Failed to get price: {e}")
        }
    }
}


async fn create_fruit(Path((fruit_name, price)): Path<(String, u32)>) -> impl IntoResponse {
    let connection = get_pool().await;
    let result = query!("INSERT INTO fruits (name, price) VALUES (?, ?)", fruit_name, price)
        .execute(connection).await;


    match result {
        Ok(result) => {
            format!("Inserted fruit. {} row(s) affected", result.rows_affected())

        }
        Err(e) => {
            format!("Failed to insert fruit: {e}")
        }
    }
}


async fn list_fruits() -> impl IntoResponse {
    let connection = get_pool().await;
    let result = query!("SELECT name, price from fruits")
        .fetch_all(connection).await;


    match result {
        Ok(all_fruits) => {
            format!("Current Fruit: {:#?}", all_fruits)
        }
        Err(e) => {
            format!("Failed to insert fruit: {e}")
        }
    }
}

#[tokio::main]
async fn main() {
    let pool = get_pool().await;

    pool.execute("CREATE TABLE IF NOT EXISTS fruits(name VARCHAR(256) NOT NULL PRIMARY KEY, price BIGINT UNSIGNED NOT NULL)").await.unwrap();


    let app = Router::new()
        .route("/fruit/:fruit_name", get(fruit))
        .route("/create_fruit/:fruit_name/:price", get(create_fruit))
        .route("/fruits", get(list_fruits));

    let addr = SocketAddr::from(([127, 0, 0, 69], 3000));

    let listener = TcpListener::bind(addr)
        .await
        .expect("Failed to start tcp server!");

    axum::serve(listener, app).await.unwrap();
    

}