The goal of this library is to allow writing relational database queries using familiar Rust syntax. The library should guarantee that a query can not fail if it compiles. This already includes preventing use after free for row ids passed between queries and even database migrations!
Writing queries using this library involves:
- Interact with row/column references as Rust values.
- Lifetimes to check the scopes of row/column references.
- Procedural mutation of row sets with methods like
filter
andjoin
.
Notably it does not involve any new syntax or macro, while still being completely type safe.
This project is under development and there are many things missing.
Query types:
- SELECT
- INSERT
- UPDATE
- DELETE
Basic operations:
- Eq, Add, Not, And, Lt, UnwrapOr, IsNotNull, AsFloat
- Everything else
Advanced operations:
- Aggregate
- Limit
Backend support:
- sqlite
- postgres
- duckdb
Despite these limitations, I am dogfooding this query builder and using it in my own project: advent-of-wasm.
Define a schema using enum
syntax:
use rust_query::migration::schema;
#[schema]
enum MySchema {
// Enum variants are database tables
User {
// This table has one column with String type.
name: String,
},
Image {
description: String,
// This column has a foreign key constraint to the User table
uploaded_by: User,
},
}
Get proof that we are running on a unique thread:
let mut client = LocalClient::try_new().unwrap();
Initialize a database:
let database = client
.migrator(Config::open("my_database.sqlite"))
.expect("database version is before supported versions")
// migrations go here
.finish()
.expect("database version is after supported versions");
Perform a transaction!
let mut transaction = client.transaction_mut(&database);
do_stuff_with_database(&mut transaction);
// After we are done we commit the changes!
transaction.commit();
Insert in the database:
// Lets make a new user 'mike',
let mike = User { name: "mike" };
let mike_id = db.insert(mike);
// and also insert a dog picture for 'mike'.
let dog_picture = Image {
description: "dog",
uploaded_by: mike_id,
};
let _picture_id = db.insert(dog_picture);
Query from the database:
// Now we want to get all pictures for 'mike'.
let mike_pictures = db.query(|rows| {
// Initially there is one empty row.
// Lets join the pictures table.
let picture = Image::join(rows);
// Now lets filter for pictures from mike,
rows.filter(picture.uploaded_by().eq(mike_id));
// and finally turn the rows into a vec.
rows.into_vec(picture.description())
});
println!("{mike_pictures:?}"); // This should print `["dog"]`.
The full example code can be found in insert_and_select.rs
Some features not shown in this example are:
- Migrations and unique constraints
- Lookups by unique constraint
- Aggregations
For more example queries you can look at the chinook example.
First download the Chinook_Sqlite.sqlite
from here and put it in the root of this repository (the working dir).
Then you can run with cargo test
.