Crates.io | sqlo |
lib.rs | sqlo |
version | 0.2.0 |
source | src |
created_at | 2023-04-15 20:39:44.249685 |
updated_at | 2023-04-16 10:30:36.015884 |
description | Syntactic sugar for sqlx. 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 and MySql are supported |
homepage | |
repository | https://github.com/jgirardet/sqlo |
max_upload_size | |
id | 840162 |
size | 171,569 |
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:
get, save, delete
methods at hand.select!, insert!, update!
: write Sq queryl with Rust code using Rust syntax and structs.#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"]}
Given this Sql database.
CREATE TABLE my_table (
id INTEGER PRIMARY KEY,
test TEXT NOT NULL,
maybe INTEGER
);
Just derive Sqlo
macro:
#[derive(Sqlo, PartialEq)]
struct MyTable {
id: i64,
text: String,
maybe: Option<i64>,
}
...
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<Maison> = 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?
Every attributes (struct or field) are expected under the Sqlo
attribute.
Sqlo expects the tablename be the struct name converted to snake_case. You can change it with tablename attribute :
#[derive(Sqlo)]
#[sqlo(tablename="another_name")]
struct MyTable {}
By default, the id
field is used as Primary Key. Change it with the primary_key
flag:
#[derive(Sqlo)]
struct MyTable {
#[sqlo(primary_key)]
name: String
}
By default, the field name is used as column name. Change it with the column
flag:
#[derive(Sqlo)]
struct MyTable {
#[sqlo(column="what_a_column_name")]
name: String
}
Function called with insert! to populate primary key field.
insert_fn
: provides callable as string which is called as primary_key value.
#[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"))
Under the hood when Sqlo
uses sqlx::query_as!
, it will use type override for the column sqlx type override so it gives select field as "field:_", ...
instead of select field, ...
.
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.
#[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:
#[derive(Sqlo)]
struct Employee {
id: i64
name: String
#[sqlo(fk="Employee"), related="manager"]
manager_id: Option<i64> // here the type is not i64 but Option<i64> since en employe may be the bosse and have no manager.
}
sqlx::query_as!
under the hood.i8
, u8
, i16
, u16
, i32
, u32
, i64
, u64
, bool
are NOT passed by reference.String
expects &str
.BString
expects &BStr
.Vec<u8>
expects &[u8]
.Option<T>
expects Option<T>
#[derive(Sqlo)]
struct MyTable {
id: i64,
name: String,
some_type: Option<String>
}
///...
Get a row by his primary_key.
Return: sqlx::Result<T>
let row = MyTable::get(&pool, 23).await?
assert_eq!(row.id, 23);
Update a full row or insert it if exists. It's an UPSERT based on primary_key.
Return: sqlx::Result<DB::QueryResult>
#[derive(Sqlo, Debug, PartialEq)]
struct MyTable {
id: i64,
name: String,
alive: bool,
members: Option<i64>
}
//...
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 a row by it's primary key.
Return: sqlx::Result<DB::QueryResult>
>>
MyTable::delete(&pool, 43).await?
Delete a row via its instance. remove
takes ownership of the instance which is not usable after.
Return: sqlx::Result<DB::QueryResult>
myrow.remove(&pool).await?;
myrow.some_field = 1; // compile_error
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 :
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:
Please refer to sqlx doc for more about it.
update!
macroIt supports the followings formats:
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
#[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?;
insert!
macroIt supports the followings formats:
#[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
select!
marcroSelect queries are performed with the select!
macro.
// query returning a derived sqlo struct
let res: Vec<MyStruct> 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.
#[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
}
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:
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 :
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.By default select!
query all the fields of a main struct. But you can query only some column if you want:
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:
struct Total {
all: i32
}
let total = select![. Total, House count(id) as all](&pool).await.unwrap();
assert_eq!(total.all, 5);
id
, width
, ...): a field.therooms.bed
): access a related field. It wil add a INNER JOINtherooms=.bed
): access a related field. It wil add a LEFT JOINHouse.width
sum(id)
, replace(adresse, "1", "345")
)id + 3
)-id
, -1
, ...match
case when thenIn 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:
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)
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 :
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:
select![. House id as id!, count(width) as total?]
//or
select![. House id!, count(width) as total?]
*
can also be used:select![.House count(*)]
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.
// 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 ::
.
// 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]
We use rust match
expression but without braces and _
as else collector.
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"]
It's an aggregate of binary expressions, here are some use cases, by SQL usage:
select![House where id == 1]
select![House where width >= 1]
select![House where width == None]
select![House where width != None]
select![House where width > 1 && width <5]
select![House where (width==1 || width==2) && height==4]
!
with parenthesis: select![House !(width>5)]
select![House where id in (1,3,4)
#
operator : select![House where name # "%bla"]
.select![House where trim(name) == "myhouse"]
&&
, ||
You can access related row/collections via a "virtual field", the specified with fk
attribute.
Sqlo supports two way of working with relationships.
JOIN
wich allowes you a direct query to some related entries.JOIN
like in regular queries.You can access related row/collections via a "virtual field", the specified with fk
attribute.
House[1]
,House[myvar]
,House[some.field]
or House[someindex[1]]
).House[1].therooms
.// select all related rooms of house where there is a bed
let a = 1;
let romms: Vec<Room> = 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...
JOIN is automagically added to queries when using a related field.
Select JOIN type with the following:
.
ex: therooms.bed
=.
(think about the inclusie =
in rust range) ex: therooms=.bed
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.
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 and might
get the error Decode(UnexpectedNullError)
.
So you have to infer nullability yourself adding ?
:
select![* House id, therooms=.id as "rooms_id?"]
Group your result with group_by
keyword followed be column or alias names.
A brackted syntax is available with []
.
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
Use the having clause just like in sql. A bracketed syntax is also availabble with []
select![.House id, sum(width) as total having total > 350]
// with foreign keys
select![.House id, count(therooms.id) as total having total > 4]
Order result with the order_by
keyword. Descending order is specified with a -
before the field name.
A brackted syntax is available with []
.
select![*House order_by -width, height]
select![*House order_by[-width, height]]
select![*House id, width as "bla:i32" order_by bla]
Use limit
clause with optional offset
separated by comma.
A brackted syntax is available with []
.
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 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
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
We support a custom page
to query by page with a mandatory page_size separated by a comma.
A brackted syntax is available with []
.
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 are done using braces {}
.
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.
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:
select![*House where zipcode where exists {ZipCodeTable zip where zip > 260}].fetch_all...
Debug all queries vith env variable :
or
In macrs, debug a single one with dbg!
.
select![dbg! * House where width >30]...
Every contribution is warmly welcomed. Please open an issue first to discuss it before you spend some time on it.
task sq-test -- some_tests -- --nocapture