#sqlx #postgresql #mysql #sqlite #orm #sql-query #compile-time

sqlo

Sqlo是sqlx的语法糖,它是为了在Rust中使用关系型数据库而创建的一个漂亮的API。Sqlo基于sqlx构建,并使用sqlx宏,这样你可以在编译时保留sqlx的全部功能,同时减少样板代码。目前支持Sqlite、Postgres和MySql。

2个不稳定版本

0.2.0 2023年4月16日
0.1.0 2023年4月15日

数据库实现 中排名 153

每月下载量 21

自定义许可证

165KB
4K SLoC

🧁 Sqlo 🍰

Sqlo是sqlx的语法糖。

内容

这是什么?

Sqlo 是在Rust中使用关系型数据库创建一个漂亮API的又一次尝试。

Sqlo 基于sqlx构建,并使用sqlx宏,这样你可以在编译时保留sqlx的全部功能,同时减少样板代码。

目前,支持Sqlite、Postgres和MySql。

它具有一些ORM-like功能,但不是真正的ORM。

主要功能

  • 几乎没有样板代码。
  • 提供 get, save, delete 方法。
  • 直观、易于使用的宏API:select!, insert!, update!:使用Rust代码和结构体以Rust语法编写Sq queryl。
  • 快速访问给定行的外键。
  • 支持sqlite、postgres和mysql

安装

#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"]}

它是如何工作的?

给定这个Sql数据库。

CREATE TABLE my_table (
  id INTEGER PRIMARY KEY,
  test TEXT NOT NULL,
  maybe INTEGER
);

仅推导 Sqlo

#[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?

推导Sqlo

结构体属性

所有属性(结构体或字段)都应位于 Sqlo 属性之下。

表名

Sqlo 预期表名是结构体名称转换为 snake_case。您可以使用 tablename 属性进行更改。

#[derive(Sqlo)]
#[sqlo(tablename="another_name")]
struct MyTable {}

字段属性

primary_key

默认情况下,使用 id 字段作为主键。可以使用 primary_key 标志进行更改

#[derive(Sqlo)]
struct MyTable {
    #[sqlo(primary_key)]
    name: String
}

column

默认情况下,字段名称用作列名称。可以使用 column 标志进行更改

#[derive(Sqlo)]
struct MyTable {
    #[sqlo(column="what_a_column_name")]
    name: String
}

insert_fn

当调用 insert! 时,将使用该函数来填充主键字段。

insert_fn:提供作为字符串的可调用对象,作为主键值调用。

#[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

在内部,当 Sqlo 使用 sqlx::query_as! 时,它将为列使用类型覆盖(sqlx 类型覆盖),因此它给出 select field as "field:_", ... 而不是 select field, ...

关系

可以指定关系并在查询中稍后使用它们。

这是通过向字段添加具有 fk 属性的外键来完成的。然后在查询中使用的相关名称将是 snake_case 相关结构体名称。例如:MyRoom=>my_room。可以使用 related 属性更改相关名称。

#[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.

存在类型检查,因此 fk 字段必须与目标结构体的主键具有相同类型(或 Option)。

实体和关系保存在在编译时创建的 .sqlo 目录中。根据编译顺序,如果在关系中对 Sqlo 实体 进行了目标但在解析之前,它可能会首次失败。只需重新构建第二次,它就会通过。

.sqlo 可选添加到 VCS。尽管这不是其主要目的,但 .sqlo 的版本控制似乎在代码更改的情况下提供了一些额外的安全性。内容是简单的 JSON 文件,非常易于阅读。

fk 文本可以是标识符("MyRoom")或路径("mycrate::mydir::MyRoom")。

使用同一结构体中声明的 fk 进行自连接

#[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!
  • 第一个参数始终是数据库连接。

参数类型

  • i8u8i16u16i32u32i64u64bool 不是按引用传递的。
  • String 预期 &str
  • BString 期望 &BStr
  • Vec<u8> 期望 &[u8]
  • Option<T> 期望 Option<T>
  • 其他所有内容都是通过引用传递的。
#[derive(Sqlo)]
struct MyTable {
    id: i64,
    name: String,
    some_type: Option<String>
}
///...

get

通过主键获取一行。

返回值:sqlx::Result<T>

let row = MyTable::get(&pool,   23).await?
assert_eq!(row.id, 23);

save

更新完整行或如果存在则插入。它基于主键的UPSERT。

返回值: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

通过主键删除一行。

返回值:sqlx::Result<DB::QueryResult>>>

MyTable::delete(&pool, 43).await?

remove

通过其实例删除一行。remove 将拥有实例的所有权,之后将无法使用。

返回值:sqlx::Result<DB::QueryResult>

myrow.remove(&pool).await?;
myrow.some_field = 1; // compile_error

宏:介绍

Sqlo 支持 select!insertupdate! 宏。我们尽量保持 API 一致,以便更容易记忆和使用。在本章中,我们将解释使用这些宏的核心原则,下一章将解释每个宏。

  • 宏仅作为 sqlx 宏 sqlx::query! 和 sqlx:query_as! 的语法糖。

  • 宏返回一个闭包,该闭包以 sqlx Executor 作为唯一参数。返回类型取决于您使用的内容,与 fetch_one, fetch_all, fetch, execute, ... 相同。

  • 这是 Rust 语法而不是 SQL:这就是我们为什么使用 == 而不是 = 的原因。

  • Sqlo 宏内容转换为 sqlx 内容

select![. House where room >23](&pool)
// is replaced with
sqlx::query_as!(House, "select * from house h where h.room > ?", 23).fetch_one(&pool)

这意味着,经过 sqlo 的检查后,sqlx 的检查将按常规进行。

  • 每个字面量、变量参数等都被作为参数传递给 sqlx 宏。

  • sqlx 方法调用选择使用与常规查询开头相同的标点符号。它遵循众所周知的正则表达式语法

    • nothing -> execute(返回无)
    • . -> fetch_one(一个)
    • * -> fetch_all(零个或多个)
    • ? -> fetch_optional(一个或零个)
    • + -> fetch(一个或多个。)

有关更多信息,请参阅 sqlx 文档

update!

它支持以下格式

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!

它支持以下格式

#[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?

如果数据库管理系统支持,主键也可以省略。

返回实例使用 .,在 SQL 中使用 insert.... returning。实际上与 MariaDB 不完全兼容

select!》宏

使用《select!》宏执行选择查询。

// 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)

让我们使用这些结构体来编写本章。

#[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
}

简介

基本上,对于平面结构体查询,它底层使用 sqlx::query_as!,只翻译查询或 sqlx::query! 用于字段/列查询。

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

请注意,它假设一个 sqlo 结构体(这里为 House),从中推断出字段/列、关系/相关字段。

一些通用规则

  • Sqlo 尝试自动通过添加 DISTINCT 来避免重复,因为重复的需求非常罕见。因此,请注意,每个《select!》查询都不会有重复的结果。

查询列

默认情况下,《select!》查询主结构体的所有字段。但如果您想查询某些列,也可以这样做

select![. House  max(width) as my_max where height > 1](&pool).await;
  • 它将使用 sqlx::query! 而不是 sqlx::query_as!

  • sqlx::query_as! 也可以用于指向另一个结构体,在结构体名称前添加逗号分隔

struct Total {
    all: i32
}
let total = select![. Total, House count(id) as all](&pool).await.unwrap();
assert_eq!(total.all, 5);
  • 我们支持以下 "列" 格式
    • 标识符(idwidth,...):一个字段。
    • 字段访问(therooms.bed):访问相关字段。这将添加一个 INNER JOIN
    • 字段访问(therooms=.bed):访问相关字段。这将添加一个 LEFT JOIN
    • 带有结构体名称的字段访问:House.width
    • SQL 函数(sum(id)replace(adresse, "1", "345")
    • 二元运算(id + 3
    • 一元:-id-1,...
    • 情况:使用 Rust match case when then

在查询的 "选择" 部分(查询的列)中,函数、操作、一元必须后面跟一个标识符 as

SQL 函数的参数可以是标识符字段、字段访问、文字("text")或任何 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)
select![* House replace(name, ::myvar, ::myarray[1]) as "new_name!:String"](&pool).await.unwrap();

但与 sqlx 不同,您不需要重复相同的复杂别名以供进一步使用

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]

作为一个方便的快捷方式,可以在别名或字段上使用 !? 而无需引号。

select![. House id as id!, count(width) as total?]
//or
select![. House id!, count(width) as total?]
  • 还可以使用 *
select![.House count(*)]

使用 Rust 项目作为参数

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

目前,索引和其他结构字段的使用必须使用 ::

// 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]

Case When Then

我们使用 Rust 的 match 表达式,但没有括号和 _ 作为 else 收集器。

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"]

子句

WHERE 子句

它是二进制表达式的聚合,以下是一些使用案例,按 SQL 使用的顺序

  • 字段:select![House where id == 1]
  • 二元运算符: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]
  • 使用括号:select![House where (width==1 || width==2) && height==4]
  • 不要在括号中使用 !select![House !(width>5)]
  • IN:select![House where id in (1,3,4)
  • LIKE:使用 # 运算符:select![House where name # "%bla"]
  • 来自连接的列:见 WHERE 子句中的 JOIN
  • 函数调用:select![House where trim(name) == "myhouse"]
  • AND, OR:使用 &&|| 连接表达式

关系

简介

您可以通过“虚拟字段”访问相关的行/集合,该字段由 fk 属性 指定。

Sqlo 支持两种处理关系的方式。

  • 第一种是不使用 JOIN,允许您直接查询一些相关条目。
  • 第二种使用与常规查询相同的 JOIN

您可以通过“虚拟字段”访问相关的行/集合,该字段由 fk 属性指定。

  • 通过索引其主键来访问行(House[1]House[myvar]House[some.field]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

当使用相关字段时,JOIN 会自动添加到查询中。

使用以下方式选择 JOIN 类型

  • 使用 . 进行 INNER JOIN,例如:therooms.bed
  • 使用 =. 进行 LEFT JOIN(类似于 Rust 范围中的包含 =)例如: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

由于 JOIN 类型需要保持一致,请注意。

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

关于 LEFT JOIN 和 Postgres 的说明:在 Postgres 中,sqlx 无法对 null 可用性做出任何假设,可能会出现 Decode(UnexpectedNullError) 错误。因此,您必须自己推断 null 可用性,并添加 ?

select![* House id, therooms=.id as "rooms_id?"]

Group By 子句

使用 group_by 关键字后跟列或别名名称来对结果进行分组。

使用括号语法 []

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

Having 子句

像在 SQL 中一样使用 having 子句。同样可以使用括号语法 []

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 By 子句

使用 order_by 关键字对结果进行排序。在字段名之前使用 - 指定降序。

使用括号语法 []

select![*House order_by -width, height]
select![*House order_by[-width, height]]
select![*House id, width as "bla:i32" order_by bla]

Limit/Offset 和分页

Limit 和 Offset

使用带有可选 offsetlimit 子句,以逗号分隔。

使用括号语法 []

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

在使用 order bylimit 一起时,sqlx 存在一个错误:每个字段都被期望为可空的,这是不正确的。目前,要处理这个用例,您必须强制为每个列设置非空性(除 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

分页

我们支持自定义 page 来通过 page 查询,必须有一个用 逗号 分隔的 page_size

使用括号语法 []

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);

子查询

子查询使用花括号 {} 完成。

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...

也可以用于返回值。

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

支持 exists 关键字

select![*House where zipcode where exists {ZipCodeTable zip where zip > 260}].fetch_all...

调试查询

使用环境变量调试所有查询

  • SQLO_DEBUG_QUERY:将显示查询是如何被翻译的
  • SQLO_DEBUG_QUERY_ALL:将显示查询是如何被翻译的 + 参数

或者

在 macrs 中,使用 dbg! 调试单个查询。

select![dbg! * House where width >30]...

贡献

每个贡献都受到热烈的欢迎。请在花费时间之前先打开一个问题来讨论。

步骤

  • 安装 taskfile
  • 克隆存储库
  • 设置开发数据库
    • task run: 设置数据库
    • task stop: 取消设置数据库
    • task reset: 取消设置 + 设置
  • 进行您的更改
  • 使用 task test: 运行所有数据库上的所有测试来测试它
  • 一些格式化
    • task clippy
    • cargo fmt
  • 备注
    • 每个命令都有其数据库专用变体:sq-check、sq-test、pg-test、pg-setup 等。支持的词首 sq、pg 和 my。
    • 由于 SQL 语法的一些特定性,每个数据库后端都有自己的迁移文件,但内容最终是相同的。
    • 帮助调试查询
  • 以以下格式在测试中获得输出:task sq-test -- some_tests -- --nocapture
  • 推送 PR。

依赖关系

~4.5–6.5MB
~119K SLoC