语句是一棵树
主线位置:查询主线的前半段。
cake::Entity::find_by_id(1)造出的Select<E>里装着一个 sea-querySelectStatement:find()往selects里放实体的每一列、往from里放表名,find_by_id对每个主键列调select.filter(col.eq(v)),filter就是cond_where,于是where字段里多了一个Expr::Binary。到这一步为止没有任何 SQL 文本,也不知道要发给哪种数据库。这一页讲这棵树由哪些类型组成;树怎么写成 SQL 在 QueryBuilder 与参数。
flowchart TB sel[SelectStatement] --> selects["selects: Vec SelectExpr"] sel --> from["from: Vec TableRef"] sel --> wh["where: ConditionHolder"] sel --> lim["limit: Option Value"] wh --> cond[Condition] cond --> bin["Expr::Binary"] bin --> col["Expr::Column"] bin --> val["Expr::Value"] col --> iden["DynIden 字符串"]
四种语句,字段就是子句
SelectStatement 的字段和 SELECT 语句的子句一一对应:
#[derive(Default, Debug, Clone, PartialEq)]
pub struct SelectStatement {
pub(crate) distinct: Option<SelectDistinct>,
pub(crate) selects: Vec<SelectExpr>,
pub(crate) into: Option<SelectInto>,
pub(crate) from: Vec<TableRef>,
pub(crate) join: Vec<JoinExpr>,
pub(crate) r#where: ConditionHolder,
pub(crate) groups: Vec<Expr>,
pub(crate) having: ConditionHolder,
pub(crate) unions: Vec<(UnionType, SelectStatement)>,
pub(crate) orders: Vec<OrderExpr>,
pub(crate) limit: Option<Value>,
pub(crate) offset: Option<Value>,
pub(crate) lock: Option<LockClause>,
pub(crate) window: Option<(DynIden, WindowStatement)>,
pub(crate) with: Option<Box<WithClause>>,
#[cfg(feature = "backend-postgres")]
pub(crate) table_sample: Option<crate::extension::postgres::TableSample>,
// ...
}
另外三种也是这样:InsertStatement 有 table、columns: Vec<DynIden>、source(VALUES 的行或一个子查询)、on_conflict、returning;UpdateStatement 有 table、from、values: Vec<(DynIden, Box<Expr>)>、where、orders、limit、returning;DeleteStatement 有 table、where、orders、limit、returning。
几个形状上的选择:
limit/offset是Option<Value>,不是Option<u64>。值和WHERE里的常量一样走参数,LIMIT写出来是$N(实验见下一页)。where和having是同一个类型ConditionHolder,groups是Vec<Expr>:子句里能出现什么,由类型决定。- 所有字段都是
pub(crate),只能通过column、from、and_where这些方法改。原因之一在下面:ConditionHolder有两种互斥的状态,混用要 panic,直接改字段就绕过了这个检查。 Debug是派生的,打印一条语句就能看到整棵树(下文实验)。
Expr:一个枚举装下所有表达式
列、值、运算、函数调用、子查询,在 sea-query 里都是 Expr 的一个分支:
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Expr {
Column(ColumnRef),
Tuple(Vec<Expr>),
Unary(UnOper, Box<Expr>),
FunctionCall(FunctionCall),
Binary(Box<Expr>, BinOper, Box<Expr>),
SubQuery(Option<SubQueryOper>, Box<SubQueryStatement>),
Value(Value),
Values(Vec<Value>),
Custom(Cow<'static, str>),
CustomWithExpr(Cow<'static, str>, Vec<Expr>),
Keyword(Keyword),
AsEnum(DynIden, Box<Expr>),
Case(Box<CaseStatement>),
Constant(Value),
TypeName(TypeRef),
}
树就是由它递归组成的:Binary 和 Unary 装着子 Expr,SubQuery 装着另一条语句。和后面几页关系最大的是三个装值的分支,它们在写 SQL 时去向不同:
| 分支 | 谁造出来 | 写 SQL 时 |
|---|---|---|
Value(Value) | 任何 Into<Value> 的东西转成 Expr:col.eq(1) 里的 1 | 变成参数 $N |
Constant(Value) | 只在 sea-query 内部:空的 Condition、LIKE ... ESCAPE 的转义字符 | 直接写进 SQL |
Custom(Cow<'static, str>) | Expr::cust("...") | 原样写进 SQL |
col.eq(1) 能成立,靠的是两层通用实现。一层是 impl<T: Into<Value>> From<T> for Expr,把 Rust 值包成 Expr::Value;另一层是 ExprTrait 对所有 T: Into<Expr> 的 blanket impl,eq、like、is_in 这些方法最后都落到 binary:
fn binary<O, R>(self, op: O, right: R) -> Expr
where
O: Into<BinOper>,
R: Into<Expr>,
{
Expr::Binary(Box::new(self.into()), op.into(), Box::new(right.into()))
}
sea-orm 的 Column::Id.eq(1) 也是在 ColumnTrait 里造出这样一个 Expr::Binary,细节在列与值。Value 各分支(每种类型一个,None 表示 NULL)也在那一页。
0.32 及以前有两个类型:SimpleExpr 是树里的节点(就是上面这个枚举),Expr 是构造用的中间值(一个装着 left: SimpleExpr、right: Option<SimpleExpr> 和运算符的结构体),不少方法只定义在 Expr 上,常要写 Expr::expr(..) 或 .into() 来回转。1.0 把它们合成一个(#889,CHANGELOG 记在 1.0.0 下),SimpleExpr 只剩一行 pub type SimpleExpr = Expr;。读 1.x 的 sea-orm(本站用的 1.1.20 配 sea-query 0.32.7)时看到的 SimpleExpr,在 2.0.4 里就是 Expr。
Condition 与 ConditionHolder
WHERE 里的东西不直接是 Expr,而是一个 Condition:
pub struct Condition {
pub(crate) negate: bool,
pub(crate) condition_type: ConditionType,
pub(crate) conditions: Vec<ConditionExpression>,
}
condition_type 是 All(用 AND 连)或 Any(用 OR 连),conditions 的每一项是一个 Expr 或一个嵌套的 Condition,negate 给整组套 NOT。Cond::all() / Cond::any() 造一个空组,add 往里加,add_option(None) 什么都不加,适合「参数有值才过滤」的写法。
语句里装条件的是 ConditionHolder,它有三种状态:
pub enum ConditionHolderContents {
#[default]
Empty,
Chain(Vec<LogicalChainOper>),
Condition(Condition),
}
Chain 是一串 And(expr) / Or(expr),只有 and_or_where(LogicalChainOper) 会走这条路。常用的 and_where 不走:它的默认实现就是 self.cond_where(other),和 sea-orm 的 filter 一样,最后都进 add_condition:
pub fn add_condition(&mut self, mut addition: Condition) {
match std::mem::take(&mut self.contents) {
ConditionHolderContents::Empty => {
self.contents = ConditionHolderContents::Condition(addition);
}
ConditionHolderContents::Condition(mut current) => {
if current.condition_type == ConditionType::All && !current.negate {
if addition.condition_type == ConditionType::All && !addition.negate {
current.conditions.append(&mut addition.conditions);
self.contents = ConditionHolderContents::Condition(current);
} else {
self.contents = ConditionHolderContents::Condition(current.add(addition));
}
} else {
self.contents = ConditionHolderContents::Condition(
Condition::all().add(current).add(addition),
);
}
}
ConditionHolderContents::Chain(_) => {
panic!("Cannot mix `and_where`/`or_where` and `cond_where` in statements")
}
}
}
三个分支对应三种情况:当前是一个没取反的 All、新来的也是,就把新条件摊平追加进去(a AND b AND c,不多一层括号);当前是 All、新来的是 Any 或取反的,就把新条件整个作为一项加进去;当前本身是 Any 或取反的,就新建一个 All 把两者都包起来。无论哪种,多次调用之间都是 AND。
filter(any![..]) 之后再 filter(..),曾经变成 OR0.27.0 之前,第二次 cond_where 会加进第一次的那个 Condition 里:第一次是 Cond::any(),第二次的条件就成了 OR 的一项。#414 用 sea-orm 报的就是这件事,标题是「Adding filters after adding a Condition::any in another call results in OR instead of AND」。修复它的 #417(首次发布于 0.27.0)在 CHANGELOG 里列为破坏性改动「Changed cond_where chaining semantics」,旧行为的注释写着「Before: confusing, since it depends on the order of invocation」,新行为是「Now: will always conjoin with AND」。上面 add_condition 的第三个分支 Condition::all().add(current).add(addition) 就是这次改出来的。
空的 Condition 也有意义。From<Condition> for Expr 把一组条件折成一个 Expr 时,空组变成常量:
} else {
Expr::Constant(match cond.condition_type {
ConditionType::Any => false.into(),
ConditionType::All => true.into(),
})
};
空的 All 是 TRUE,空的 Any 是 FALSE,和数学上空的合取、析取一致。动态拼条件时 Cond::any() 一项都没加上,生成的是 WHERE FALSE,不是语法错误的 WHERE。(is_in 一个空列表是另一条路,写 SQL 时换成 1 = 2,见下一页。)
M2 Max、macOS、rustc 1.99.0,sea-query 1.0.2(src/bin/tree.rs 的后半段):
let w = Query::select()
.column("id")
.from("cake")
.cond_where(Cond::any().add(Expr::col("a").eq(1)).add(Expr::col("b").eq(2)))
.and_where(Expr::col("c").eq(3))
.cond_where(Cond::all().add_option(None::<Expr>))
.to_owned();
println!("where : {}", w.to_string(PostgresQueryBuilder));
let e = Query::select().column("id").from("cake").cond_where(Cond::all()).to_owned();
println!("empty : {}", e.to_string(PostgresQueryBuilder));
第一行的 WHERE 后面是什么?第三次调用加进去的空 Cond::all() 会不会变成一个 AND TRUE?第二行呢?
Chain 状态和 Condition 状态不能混用,add_and_or 和 add_condition 遇到对方的状态都会 panic。panic 消息里点名的是 and_where,但如上所说 and_where 本身走的是 cond_where,真正会触发它的是 and_or_where(src/bin/mix.rs):
let q = Query::select()
.column("id")
.from("cake")
.and_where(Expr::col("a").eq(1))
.and_or_where(LogicalChainOper::Or(Expr::col("b").eq(2)))
.to_owned();
thread 'main' (3569773) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-query-1.0.2/src/query/condition.rs:677:17:
Cannot mix `and_where`/`or_where` and `cond_where` in statements
消息里的 or_where 早已删掉(0.27.0 的 CHANGELOG:「Removed deprecated methods (or_where, or_having, table_column etc)」),消息没跟着改。要写 OR,用 Cond::any()。
Iden 与 DynIden
表名、列名、别名在树里都是 DynIden:
/// A prepared (quoted) identifier string.
///
/// The naming is legacy and kept for compatibility.
/// This used to be an alias for a `dyn Iden` object that's lazily rendered later.
///
/// Nowadays, it's an eagerly-rendered string.
/// Most identifiers are static strings that aren't "rendered" at runtime anyway.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DynIden(pub(crate) Cow<'static, str>);
它从 Iden 转来,From<T: Iden> 只做一件事:DynIden(iden.quoted())。Iden 有两个方法:
pub trait Iden {
// ...
/// If the identifier doesn't need to be escaped, return `'static str`.
/// This can be deduced at compile-time by the `Iden` macro,
/// or using the [`is_static_iden`] function.
///
/// `Cow::Owned` would always be escaped.
fn quoted(&self) -> Cow<'static, str> {
Cow::Owned(self.to_string())
}
// ...
fn unquoted(&self) -> &str;
}
名字叫 quoted,返回的却不带引号。它用 Cow 的两个分支传一个标志:Borrowed 表示「这个名字只含安全字符,写的时候不用逐字检查」,Owned 表示「写的时候要转义」。&'static str 的实现用 is_static_iden 判断:以字母或 _ 开头、后面只有字母数字和 _ 才算安全。#[derive(Iden)] 和 sea-orm 的 DeriveColumn 在编译期做同样的判断。Alias 总是返回 Owned。写 SQL 时怎么用这个标志,见下一页的 prepare_iden。
Debug 输出能看到这一点:树里存的就是名字本身,不是一个待渲染的对象(src/bin/tree.rs):
let q = Query::select()
.column("id")
.from("cake")
.and_where(Expr::col("id").eq(1))
.to_owned();
println!("{q:#?}");
SelectStatement {
distinct: None,
selects: [
SelectExpr {
expr: Column(
Column(
ColumnName(
None,
DynIden(
"id",
),
),
),
),
alias: None,
window: None,
},
],
into: None,
from: [
Table(
TableName(
None,
DynIden(
"cake",
),
),
None,
),
],
join: [],
where: ConditionHolder {
contents: Condition(
Condition {
negate: false,
condition_type: All,
conditions: [
Expr(
Binary(
Column(
Column(
ColumnName(
None,
DynIden(
"id",
),
),
),
),
Equal,
Value(
Int(
Some(
1,
),
),
),
),
),
],
},
),
},
groups: [],
having: ConditionHolder {
contents: Empty,
},
unions: [],
orders: [],
limit: None,
offset: None,
lock: None,
window: None,
with: None,
table_sample: None,
index_hints: {},
}
and_where 一次,where 就是 Condition 状态、一个 All 组;1 是 Value(Int(Some(1))),还没有变成 $1。列引用是 ColumnName(Option<TableName>, DynIden),表名是 TableName(Option<SchemaName>, DynIden):schema、表、列逐层可选。0.32.7 里这些组合是平铺的枚举分支,比如 TableRef::SchemaTableAlias(DynIden, DynIden, DynIden)、ColumnRef::SchemaTableColumn(..)。
DynIden 原来是一个 dyn Iden1.0 之前,DynIden 是 SeaRc<dyn Iden>(Rc 或 Arc 包一个 trait 对象),名字到写 SQL 时才调 Iden::unquoted(&self, s: &mut dyn Write) 渲染出来。#909(CHANGELOG 记在 1.0.0 下)把它改成现在的样子,CHANGELOG 写的是「Previously, DynIden is an alias to SeaRc<dyn Iden>, and is lazily rendered. Now, it's an Cow<'static, str>, and is eagerly rendered.」,并说明「As such, Send / Sync is no longer needed」。PR 的说明是「The biggest behaviour change is late binding to early binding」,以及「For majority of usecases in SeaORM though, since they're 'static str anyway, there should be no difference.」Iden::unquoted 的签名也随之变成上面的 fn unquoted(&self) -> &str,手写 Iden 的代码升级时要改。
方言直到 build 才出现
语句里没有一个字段说它属于哪种数据库。同一个值交给三个 QueryBuilder,得到三种 SQL(src/bin/tree.rs 接着上面的 q):
println!("pg : {:?}", q.build(PostgresQueryBuilder));
println!("mysql : {:?}", q.build(MysqlQueryBuilder));
println!("sqlite : {:?}", q.build(SqliteQueryBuilder));
pg : ("SELECT \"id\" FROM \"cake\" WHERE \"id\" = $1", Values([Int(Some(1))]))
mysql : ("SELECT `id` FROM `cake` WHERE `id` = ?", Values([Int(Some(1))]))
sqlite : ("SELECT \"id\" FROM \"cake\" WHERE \"id\" = ?", Values([Int(Some(1))]))
引号和占位符不同,参数一样。sea-orm 正需要这样:Select<E> 在构造时不知道会被交给哪个连接,DatabaseConnection 的后端是运行时从连接串决定的(同一个程序里可以同时有 Postgres 连接和 MockDatabase)。sea-orm 的 StatementBuilder 在执行前按 DbBackend 分派,build_any_stmt! 就是一个 match:
macro_rules! build_any_stmt {
($stmt: expr, $db_backend: expr) => {
match $db_backend {
DbBackend::MySql => $stmt.build(MysqlQueryBuilder),
DbBackend::Postgres => $stmt.build(PostgresQueryBuilder),
DbBackend::Sqlite => $stmt.build(SqliteQueryBuilder),
}
};
}
ConnectionTrait::query_one 先 get_database_backend(),再 db_backend.build(stmt),得到 sea-orm 的 Statement(SQL 字符串加 Values)。这个 Statement 怎么交给 sqlx,见绑到 sqlx。
方言相关的东西也有漏进树里的:SelectStatement 的 table_sample 字段带着 #[cfg(feature = "backend-postgres")],BinOper 有 PgOperator(PgBinOper) 分支(ILIKE、@>、-> 等)。这些是「只有某个方言有的语法」,树允许表达它们,写 SQL 时由对应的 QueryBuilder 认领:table_sample 只有 PostgresQueryBuilder 的 prepare_table_sample 会写,QueryBuilder 的默认实现是空函数。
动脑筋答案
where : SELECT "id" FROM "cake" WHERE ("a" = 1 OR "b" = 2) AND "c" = 3
empty : SELECT "id" FROM "cake" WHERE TRUE
- 第一次
cond_where时 holder 是Empty,Any组原样放进去。 and_where(c = 3)就是cond_where,新来的是一个All组;当前是Any,走第三个分支,新建All把两者包起来:(a OR b) AND c。括号是写 SQL 时按优先级加的。- 第三次加的是一个空的
All组,当前已经是没取反的All,走第一个分支,append一个空Vec,什么都没加,所以没有AND TRUE。 - 只有一个空的
Cond::all()时,折成Expr是Constant(true),写成TRUE。它是Constant而不是Value,build时也不会变成参数。
小结
- 四种语句的字段就是 SQL 的子句,全是
pub(crate),只能通过方法改;limit/offset存的是Value。 - 所有表达式是一个
Expr枚举,1.0 起SimpleExpr是它的别名。装值的分支有三个:Value写成参数,Constant和Custom直接写进 SQL。 and_where和 sea-orm 的filter都是cond_where,多次调用总用AND连(0.27.0 起);空All是TRUE,空Any是FALSE;and_or_where不能和它们混用,会 panic。DynIden是提前渲染好的Cow<'static, str>,Borrowed/Owned表示写的时候要不要转义。- 语句不带方言,sea-orm 在执行前按连接的
DbBackend选QueryBuilder。