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 }
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"); }
#![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
, andrusqlite
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();
}