🧁 Sqlo 🍰
Syntactic sugar for sqlx.
#### Contents
- [Install](#install)
- [Deriving Sqlo](#deriving-sqlo)
- [Relations](#relations)
- [Methods](#methods): **[get](#get) [save](#save) [delete](#delete) [remove](#remove)**
- [Macros](#macros-introduction): **[insert!](#the-insert-macro) [update!](#the-update-macro) [select!](#the-select-marcro)**
- [Clauses](#clauses):
[where](#the-where-clause)
[join](#relationship)
[goupe by](#the-group-by-clause)
[having](#the-having-clause)
[pagination](#limitoffset-and-pagination)
[subquery](#subqueries)
## What is it ?
**Sqlo** is another attempt to make a nice/pleasant API in Rust using relational database.
**Sqlo** is built on top of _sqlx_ and uses sqlx macros so you keep all the power of _sqlx_ at compile time with less boiler plate.
Right now, Sqlite, Postgres, MySql are supported.
It has some ORM-like capabilities without being a real ORM.
Main features:
- Almost no boilerplate.
- `get, save, delete` methods at hand.
- Intuitive, easy to use macros api : `select!, insert!, update!` : write _Sq queryl_ with _Rust_ code using _Rust_ syntax and structs.
- Quick access to foreinkeys for a given row.
- supports sqlite, postgres and mysql
## Install
```toml
#Cargo.toml
sqlo = {version="0.1.0", features=["sqlite"]}
// or
sqlo = {version="0.1.0", features=["postgres"]}
// or
sqlo = {version="0.1.0", features=["mysql"]}
```
## How it works ?
Given this Sql database.
```sql
CREATE TABLE my_table (
id INTEGER PRIMARY KEY,
test TEXT NOT NULL,
maybe INTEGER
);
```
Just derive `Sqlo` macro:
```rust
#[derive(Sqlo, PartialEq)]
struct MyTable {
id: i64,
text: String,
maybe: Option,
}
...
use sqlo::{select, update, insert};
//
let pool = get_my_db_pool().await?;
// create row
let a = insert!(. MyTable text="hello")(&pool).await?;
// retrieve row by primary_key
let mut b = MyTable::get(&pool, a.id).await?
//or
let mut b = select!(.MyTable[1])(&pool).await // the `.` means fetch_one
assert_eq!(a,b);
// update a full row with instance
b.text = "bye".to_string();
b.save(&pool).await?;
// select: where order limit
let items : Vec = select![* Maison where text=="bla" order_by -id limit 50](&pool).await?;
// `*` means fetch_all, use `+` for fetch, `.` for `fetch_one`
// select: sql function, group_by, force non null alias.
let items = select![*PieceFk maison_id, count(*) as total! group_by maison_id having total >3 order_by total](&p.pool).await?;
// or
let items = select![*PieceFk maison_id, count(*) as "total!:i32" group_by maison_id having total >3 order_by total](&p.pool).await?;
// Aliases can be reused along the query
// update with instance (parenthesis)
update![ MyTable(b) text="I'm Back", maybe=Some(12)](&pool).await?; // No `.,+,*` means `execute`.
// or with primary_key (brackets)
let c = update[. MyTable[b.id] text="I'm Back", maybe=Some(12)](&pool).await?; // `.` means fetch_one
// remove by instance
c.remove(&pool).await?
//or delete with pk
MyTable::delete(&pool, pk).await?
```
## Deriving Sqlo
#### Struct attributes
Every attributes (struct or field) are expected under the `Sqlo` attribute.
##### tablename
Sqlo expects the tablename be the struct name converted to snake_case.
You can change it with tablename attribute :
```rust
#[derive(Sqlo)]
#[sqlo(tablename="another_name")]
struct MyTable {}
```
### Fields Attribute
#### primary_key
By default, the `id` field is used as Primary Key. Change it with the `primary_key` flag:
```rust
#[derive(Sqlo)]
struct MyTable {
#[sqlo(primary_key)]
name: String
}
```
#### column
By default, the field name is used as column name. Change it with the `column` flag:
```rust
#[derive(Sqlo)]
struct MyTable {
#[sqlo(column="what_a_column_name")]
name: String
}
```
#### insert_fn
Function called with insert! to populate primary key field.
`insert_fn`: provides callable as string which is called as primary_key value.
```rust
#[derive(Sqlo)]
struct MyTable {
#[sqlo(insert_fn="uuid::Uuid::new_v4")]
id: Uuid,
name: String
}
//...
let instance = insert!(.MyTable name="some string")(&p.pool).await.unwrap();
assert_eq!(instance.id, Uuid("someuuidv4"))
```
#### type_override
Under the hood when `Sqlo` uses `sqlx::query_as!`, it will use type override for the column [sqlx type override](https://docs.rs/sqlx/latest/sqlx/macro.query_as.html#column-type-override-infer-from-struct-field) so it gives `select field as "field:_", ...` instead of `select field, ...`.
## Relations
Relations can be specified and used later in queries.
It's done by adding a foreign key with `fk` attribute to a field. Related name in queries will then be the snake_case related struct name. For example: MyRoom=>my_room. The related name can be changed with the `related` attribute.
```rust
#[derive[Sqlo, Debug, PartialEq]]
struct House {
id: i64,
name: String,
width: i64,
height: i64
}
#[derive[Sqlo, Debug, PartialEq]]
struct Room {
id: i64,
#[sqlo(fk = "House")]
house_id: i64
}
// will use myhouse.room in queries
// or
#[derive[Sqlo, Debug, PartialEq]]
struct Room {
id: i64,
#[sqlo(fk = "House", related = "therooms")]
house_id: i64
bed: bool
}
// will use myhouse.therooms in queries.
```
There is a type check so the `fk` field must have the same type as target struct's primary key (or an `Option`).
Entities and Relations are kept in a `.sqlo` directory which is created at compile time. Depending the order of compilation,it might fails at first glance if a `Sqlo Entity` is targeted in a relation but not yet parsed . Just rebuild a second time and it will pass.
`.sqlo` may or not be added to VCS. Although it isn't its primary purpose, versionning `.sqlo` appears to add some more security in case of code change. The content is simple json files, which are very easy to read.
The `fk` literal can be identifier (`"MyRoom"`) or a path (`"mycrate::mydir::MyRoom"`).
Use self-joins declaring `fk` in the same struct:
```rust
#[derive(Sqlo)]
struct Employee {
id: i64
name: String
#[sqlo(fk="Employee"), related="manager"]
manager_id: Option // here the type is not i64 but Option since en employe may be the bosse and have no manager.
}
```
## Methods
### Introduction
- Every method returning an instance of the derived struct uses `sqlx::query_as!` under the hood.
- The first parameter is always the database connection.
### Parameters type
- `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `bool` are NOT passed by reference.
- `String` expects `&str`.
- `BString` expects `&BStr`.
- `Vec` expects `&[u8]`.
- `Option` expects `Option`
- Everything else is passed by reference.
```rust
#[derive(Sqlo)]
struct MyTable {
id: i64,
name: String,
some_type: Option
}
///...
```
### get
Get a row by his primary_key.
Return: `sqlx::Result`
```rust
let row = MyTable::get(&pool, 23).await?
assert_eq!(row.id, 23);
```
### save
Update a full row or insert it if exists. It's an UPSERT based on primary_key.
Return: `sqlx::Result`
```rust
#[derive(Sqlo, Debug, PartialEq)]
struct MyTable {
id: i64,
name: String,
alive: bool,
members: Option
}
//...
let mut mytable = MyTable{id:1, name:"bla".to_string(), alive:true, membres:None};
mytable.save(&pool).await?;
// doesn't exists then equivalent to insert!(Mytable id=1, name="bla", alive=true)(&pool).await?
mytable.members = Some(345);
mytable.save(&pool);
// equivalent to update!(MyTable(mytable) members=Some(345))(&p.pool).await?
let mytable2 = MyTable::get(&pool, 1).await?;
assert_eq!(mytable, mytable2);
```
### delete
Delete a row by it's primary key.
Return: `sqlx::Result`>>
```rust
MyTable::delete(&pool, 43).await?
```
### remove
Delete a row via its instance. `remove` takes ownership of the instance which is not usable after.
Return: `sqlx::Result`
```rust
myrow.remove(&pool).await?;
myrow.some_field = 1; // compile_error
```
## Macros: Introduction
Sqlo supports `select!`, `insert` and `update!` macro.
We try keep API consistent to make it easy to remember and use.
In this chapter we'll explain the core principles of using those macros, next chapter will explain each one.
- Macros only act as syntactic sugar to sqlx macros `sqlx::query! and sqlx:query_as!`.
- Macros return a closure which takes an `sqlx Executor` as unique argument. Return type is the same as `fetch_one, fetch_all, fetch, execute, ...` depending what you use.
- It's rust syntax not sql: that's why we use `==` instead of `=`.
- Sqlo macro content is translated to sqlx content :
```rust
select![. House where room >23](&pool)
// is replaced with
sqlx::query_as!(House, "select * from house h where h.room > ?", 23).fetch_one(&pool)
```
It means, after sqlo's checks, sqlx's checks will occur as usual.
- Every literal, variable arguments, ... are passed as argument to sqlx macros.
- sqlx method call's choice is donc using punctuations sign like in regular at the beggining of the query. It follows welle known regular expressions syntax:
- nothing -> _execute_ (returns nothing)
- **\.** -> _fetch_one_ (one)
- **\*** -> _fetch_all_ (zero or more)
- **\?** -> _fetch_optional_ (one or zero)
- **\+** -> _fetch_ (one or more.)
Please refer to [sqlx doc](https://docs.rs/sqlx/latest/sqlx/macro.query.html) for more about it.
## The `update!` macro
It supports the followings formats:
```rust
update![TableStruct[instance_id] field1=value1, field2=value2](&pool).await?
// with square bracket instance id is a u32, string, &str, Uuid, ....
update![TableStruct(instance) field1=value1, field2=value2](&pool).await?
// use an instance of TableStruct, primary_key is deduced.
// this format takes ownership of instance sor you can't use instance after.
// To reuse instance you have to specify a return (fetch_one, fetch_all, fetch)
let instance = update![. TableStruct(instance) field1=value1, field2=value2](&pool).await?
// not the dot `.` meaning fetch_one
// only with sqlite and postgres
```
```rust
#[derive[Sqlo, Debug, PartialEq]]
struct House {
id: i64,
name: String,
width: i64,
height: i64
}
//...
let house = House::get(&pool, 2);
let house = update![. House(house) name= "bla", width=34](&pool).await?;
let big_height = 345;
update_House!(House[2] height=big_height)(&pool).await?;
//or
update_House!(House[2] height=::big_height)(&pool).await?;
```
## The `insert!` macro
It supports the followings formats:
```rust
#[derive[Sqlo, Debug, PartialEq]]
struct House {
#[sqlo(insert_fn="some::func::to_create_ids")]
id: i64,
name: String,
width: i64,
height: Some(i64)
}
// with all fiekds
insert![House, id=1, name="bla", width=23, height=34](&pool).await?
// with all fields, None explicit
insert![House, id=1, name="bla", width=23, height=None](&pool).await?
// with all fields, None implicit
insert![House, id=1, name="bla", width=23](&pool).await?
// using the `insert_fn` for primary key
insert![House, name="bla", width=23, height=None](&pool).await?
// returning instance
let house = insert![. House, name="bla", width=23, height=None](&pool).await?
// with variable
let a = 1;
insert![House,id=::a name="bla", width=23, height=None](&pool).await?
//or
insert![House,id=a name="bla", width=23, height=None](&pool).await?
```
Primary_key can also be ommited, if supported by the DBMS.
Returning instance with `.` uses `insert.... returning` in SQL.
Actually not fully fonctional with [MariaDB](https://github.com/launchbadge/sqlx/issues/2457)
## The `select!` marcro
Select queries are performed with the `select!` macro.
```rust
// query returning a derived sqlo struct
let res: Vec select![* MyStruct where myfield > 1](&pool).await.unwrap();
// select * from mystruct_table where mystruct_table.myfield >1
// query some specific values/column
let res = select![. MyStruct max(some_field) as bla where something == 23](&pool).await.unwrap();
assert_eq!(res.bla, 99)
```
Let's use theese struct for this chapter.
```rust
#[derive[Sqlo, Debug, PartialEq]]
struct House {
id: i64,
name: String,
width: i64,
height: i64,
zipcode: i64
}
struct Room {
id: i64,
#[sqlo(fk = "House", related = "therooms")]
house_id: i64
bed: bool
}
```
### Introduction
Basically for plain struct query, it uses `sqlx::query_as!` under the hood and just translate the query or `sqlx::query!` for field/column querys:
```rust
select![* House where bed == true].await
//roughly is translated into
query_as![House, "SELECT DISTINCT id, name, width, height FROM house where bed=?", true].fetch_all(&p.pool).await;
select![. House max(width) as width_max where height > 1](&pool).await;
//roughly is translated into
query!["SELECT DISTINCT max(width) AS width_max FROM house where height > ?", 1].fetch_one(&pool).await
```
Please keep in mind that is assumes a **main** sqlo struct (`House` here) from which field/column, relation/related fields are deduced.
Some generals rules :
- Sqlo tries to avoid duplicates automatically by adding`DISTINCT` when it's necessary since the need of duplicates is very rare. So keep in mind that every `select!` query won't have duplicated result.
### Query column
By default `select!` query all the fields of a main struct. But you can query only some column if you want:
```rust
select![. House max(width) as my_max where height > 1](&pool).await;
```
- It will use `sqlx::query!` not `sqlx::query_as!`.
- But `sqlx::query_as!` can also be used targeting another struct adding the struct name the beginning separeted by a coma:
```rust
struct Total {
all: i32
}
let total = select![. Total, House count(id) as all](&pool).await.unwrap();
assert_eq!(total.all, 5);
```
- we support the following "column" format:
- identifier (`id`, `width`, ...): a field.
- a field access (`therooms.bed`): access a related field. It wil add a [INNER JOIN](#using-join)
- a field access (`therooms=.bed`): access a related field. It wil add a [LEFT JOIN](#using-join)
- a field acces with the struct name: `House.width`
- a sql function (`sum(id)`, `replace(adresse, "1", "345")`)
- a binary operation(`id + 3`)
- unary: `-id`, `-1`, ...
- case: use rust `match` [case when then](###Case-When-Then)
In the "select" part of the query (the columns queried), function, operation, unary must be followed by `as` with an identifier.
Sql function'a parameters can bien identifier field, field access, literal (`"text"`) or any rust expression (array indexing, instance field access, simple variable). In this last case could be escaped with a `::` if needed:
```rust
let myvar = "bla".to_string();
let myarray = ["bli", "ble", "blo"];
select![* House replace(name, ::myvar, ::myarray[1]) as new_name](&pool).await.unwrap();
//sqlx::query!["SELECT REPLACE(name, ?, ?) as new_name FROM house", myvar, myarray[1]].fetch_all(&pool)
```
- [Sqlx's overrides](https://docs.rs/sqlx/latest/sqlx/macro.query.html#overrides-cheatsheet) can be used exactly in the same way:
```rust
select![* House replace(name, ::myvar, ::myarray[1]) as "new_name!:String"](&pool).await.unwrap();
```
but unlike `sqlx` you don't have to repeat the same complex alias for further use :
```rust
sqlx::query![r#"SELECT id, count(width) as "total!:i32" group by "total!:i32" "#]
//instead with sqlo, just repeat the alias name without type indication
select![. House id, count(width) as "total!:i32" group_by total]
```
As a convenience shortcut `!` and `?` can be used without quotes on alias or directly on field:
```rust
select![. House id as id!, count(width) as total?]
//or
select![. House id!, count(width) as total?]
```
- `*` can also be used:
```rust
select![.House count(*)]
```
### Using Rust items as parameters:
Rust items can be passed to expressions. If a field and a variable have the same name, add `::` before the name to force usage of the variable instead of field.
```rust
// Variables
let width = 34;
select![* House where height == ::width] // Right hand part of the expression will refere to the variable width not the field `width` of struct House
select![* House where width == ::width]
select![.House where id == ::width] // variable width is used
// sql : select * from house where id=? (? will be 34 as parameter)
select![.House where id == width] // variable width is ignored, column name wil be used in sql
// sql : select * from house where id=width
```
For now index and other struct field usage must use `::`.
```rust
// Indexing
let array = [1 , 2, 3]
select![. House where width == ::array[0]]
// struct field
struct A {b:i32}
let a = A{b:2}
select![. House where width == ::a.b]
```
```rust
```
### Case When Then
We use rust `match` expression but without braces and `_` as else collector.
```rust
select[.House id, match width 33=>"small", 100=>"big", _=>"don't know" as "how_big:String"]
//sqlx::query![r#"SELECT id, CASE width WHEN ? THEN ? WHEN ? THEN ? ELSE ? END as "how_big:String""#,33,"small",100, "big", "dont know"]
select[.House id, match width<33=>"small", width<100=>"big", _=>"very big" as "how_big:String"]
//sqlx::query![r#"SELECT id, CASE WHEN house.width THEN ? WHEN house.width THEN ? ELSE ? END as "how_big:String""#,33,"small",100, "big", "very big"]
```
## Clauses
### The WHERE clause
It's an aggregate of binary expressions, here are some use cases, by SQL usage:
- field: `select![House where id == 1]`
- binary operator: `select![House where width >= 1]`
- IS NULL: `select![House where width == None]`
- IS NOT NULL: `select![House where width != None]`
- BETWEEN: `select![House where width > 1 && width <5]`
- use of parenthesis: `select![House where (width==1 || width==2) && height==4]`
- NOT use `!` with parenthesis: `select![House !(width>5)]`
- IN : `select![House where id in (1,3,4)`
- LIKE: use `#` operator : `select![House where name # "%bla"]`.
- column from join: see [JOIN in where clause](#using-join)
- function call: `select![House where trim(name) == "myhouse"]`
- AND, OR: chain expressions with `&&`, `||`
### Relationship:
#### Introduction
You can access related row/collections via a "virtual field", the specified with [`fk` attribute](#relations).
Sqlo supports two way of working with relationships.
- the first one without `JOIN` wich allowes you a direct query to some related entries.
- the second one uses `JOIN` like in regular queries.
#### Retrieving related rows without JOIN
You can access related row/collections via a "virtual field", the specified with `fk` attribute.
- A row is accessed by indexing its primary key (`House[1]`,`House[myvar]`,`House[some.field]` or `House[someindex[1]]`).
- "virtual" related fielda is accessed by its related name: `House[1].therooms`.
```rust
// select all related rooms of house where there is a bed
let a = 1;
let romms: Vec = select![* House[a].therooms where bed == true](&pool).await.unwrap();
//sqlx::query_as![Room, r#"SELECT * FROM room where id=? AND bed=?"#, a, true].fetch_all...
```
#### Using JOIN
JOIN is automagically added to queries when using a related field.
Select JOIN type with the following:
- INNER JOIN with `.` ex: `therooms.bed`
- LEFT JOIN with `=.` (think about the inclusie `=` in rust range) ex: `therooms=.bed`
```rust
select![* House where therooms.bed == true]
// sqlx::query_as![House, "SELECT * FROM house INNER JOIN room ON house.id=room.maison_id WHERE room.bed == ?", true].fetch_all
select![ * House where width>3 && therooms=.bed == true]
// sqlx::query_as![House, "SELECT * FROM house LEFT JOIN room ON house.id=room.maison_id WHERE house.width> ? AND room.bed == ?", 3, true].fetch_all
select![. House id, count(therooms.id) as total]
// sqlx::query_as![House, "SELECT maison.id, count(room.id) as total FROM house JOIN room ON house.id=room.maison_id"].fetch_one
```
Since JOIN type needs to stick the same please pay attention to it.
```rust
select![* House id, therooms.id where therooms=.bed == true] // BAD you use to different joins INNER and LEFT (sqlx will fail)
select![* House id, therooms=.id where therooms=.bed == true] // GOOD : the join is expressed in the same way
```
Note About LEFT JOINS and Postgres:
[With Postgres, sqlx can't make any assumption of nullability](https://github.com/launchbadge/sqlx/issues/367#issuecomment-799829096) and might
get the error `Decode(UnexpectedNullError)`.
So you have to infer nullability yourself adding `?` :
```rust
select![* House id, therooms=.id as "rooms_id?"]
```
### The Group By clause
Group your result with `group_by` keyword followed be column or alias names.
A brackted syntax is available with `[]`.
```rust
select![.House width, count(id) as "total!:i32" group_by width order_by total]
select![.House name, count(therooms.house_id) as total group_by name] // follows foreign keys
```
### The Having clause
Use the having clause just like in sql. A bracketed syntax is also availabble with `[]`
```rust
select![.House id, sum(width) as total having total > 350]
// with foreign keys
select![.House id, count(therooms.id) as total having total > 4]
```
### The Order by clause
Order result with the `order_by` keyword. Descending order is specified with a `-` before the field name.
A brackted syntax is available with `[]`.
```rust
select![*House order_by -width, height]
select![*House order_by[-width, height]]
select![*House id, width as "bla:i32" order_by bla]
```
### Limit/Offset and Pagination
#### Limit and Offset
Use `limit` clause with optional `offset` separated by **comma**.
A brackted syntax is available with `[]`.
```rust
select![*House limit 5] // SELECT * FROM house LIMIT 5
select![*House limit 5,8] // SELECT * FROM house LIMIT 5 OFFSET 8
select![*House limit[5,8]] // SELECT * FROM house LIMIT 5 OFFSET 8
```
There is [a bug in sqlx](https://github.com/launchbadge/sqlx/issues/1126#issuecomment-1450905220) when using `order by`
and `limit` togther: Every field is expected to be nullable which is wrong. Right now to handle this use case you'll have
to force non nullabilty for each column (except Option fields).
```rust
select![*House, House id as "id!", width as "width!", height as "height!", name as "name!" order_by name limit 4]
// when using fields `select!` uses query_as! behind the back so reinforce using query_as! with House
```
#### Pagination
We support a custom `page` to query by _page_ with a mandatory _page_size_ separated by a **comma**.
A brackted syntax is available with `[]`.
```rust
let limit = select![*House limit 2,4].fetch_all(&p.pool).await.unwrap();
let page = select![*House page 3,2].fetch_all(&p.pool).await.unwrap(); //means page 3 with page size of 2.
// will both select 5th et 6th entries.
assert_eq!(limit, page);
```
### Subqueries
Subqueries are done using braces `{}`.
```rust
select![*House where zipcode in {ZipCodeTable zip where zip > 260}](&pool)...
// transltates to
// sqlx::query_as!(House, "select * from house where zipcode in (select distinct zip from zip_table where zip > ?)", 260 ).fetch_all...
```
Can be used as well in the returned value.
```rust
select![*House id, {HouseKind count(*) where width == House.width} as kind_total ]
// a few notes here :
// - it needs an alias since it's returned
// - use the struct name to leverage ambigous fields (here width)
// - no `as` is required in the subquery since it's not returned
```
It supports `exists` keyword:
```rust
select![*House where zipcode where exists {ZipCodeTable zip where zip > 260}].fetch_all...
```
## Debugging Queries
Debug all queries vith env variable :
- SQLO_DEBUG_QUERY: will show you how queries are translated
- SQLO_DEBUG_QUERY_ALL: will show you how queries are translated + the params
or
In macrs, debug a single one with `dbg!`.
```rust
select![dbg! * House where width >30]...
```
## Contributing
Every contribution is warmly welcomed.
Please open an issue first to discuss it before you spend some time on it.
### steps
- install [taskfile](https://taskfile.dev)
- clone the repo
- setup development database:
- task run: setup databases
- task stop: unsetup databases
- task reset: unsetup + setup
- Make you changes
- test it with task test: run all tests on every databases
- some formatting:
- task clippy
- cargo fmt
- remarques :
- Every command has its database only variant : sq-check, sq-test, pg-test, pg-setup, ... Supported prefixes sq, pg and my.
- Due to some specificities in SQL syntax, each database backend has is own migrations file but the content is at the end the same.
- [help to debug queries](#debugging-queries)
- Get output in tests with the following format:
`task sq-test -- some_tests -- --nocapture`
- push the PR.