源码地图
主线位置:不在主线上。本页说明三条主线经过的代码分在哪几个 crate、哪些目录,crate 之间怎么依赖,feature 怎么把它们拼起来,以及同步版本
sea-orm-sync是怎么来的。
sea-orm 仓库在 tag 2.0.4 的根 Cargo.toml 是一个工作区,members = [".", "sea-orm-macros", "sea-orm-codegen", "sea-orm-arrow"];sea-orm-migration、sea-orm-cli、sea-orm-sync、sea-orm-rocket 各自的 Cargo.toml 里又写着自己的 [workspace],是同一个仓库里的独立工作区。sea-query 在另一个仓库,sea-schema 在第三个仓库。和本卷有关的 crate 之间是这样依赖的(实线是普通依赖,虚线是可选依赖):
flowchart TB app["你的 crate"] --> orm["sea-orm"] app --> mig["sea-orm-migration"] orm --> mac["sea-orm-macros"] orm --> sq["sea-query"] orm -.->|"sqlx-postgres 等"| sqs["sea-query-sqlx"] orm -.->|"sqlx-postgres 等"| sqlx["sqlx"] orm -.->|"schema-sync"| schema["sea-schema"] sqs --> sq sqs --> sqlx mig --> orm mig --> schema mig -.->|"cli"| cli["sea-orm-cli"] cli --> gen["sea-orm-codegen"] cli --> schema cli --> sqlx gen --> sq
各个 crate
| crate | 版本 | 管什么 | 源码行数 |
|---|---|---|---|
sea-orm | 2.0.4 | 运行时:实体 trait、查询与写入的构造器、执行器、连接与事务、驱动适配、从实体建表 | src/ 45 625 |
sea-orm-macros | 2.0.4 | 过程宏:DeriveEntityModel、#[sea_orm::model]、DeriveRelation、DeriveActiveEnum、DerivePartialModel、FromQueryResult 等 | src/ 12 665 |
sea-query | 1.0.2 | 与数据库无关的语句树,加上 MySQL、Postgres、SQLite 三个 QueryBuilder | src/ 33 400 |
sea-query-sqlx | 0.9.1 | 把 sea-query 的 Values 绑成 sqlx 的参数(SqlxValues) | src/ 937 |
sea-query-derive | 1.0.0 | sea-query 的 #[derive(Iden)] 等 | src/ 2 339 |
sea-orm-migration | 2.0.4 | MigrationTrait、MigratorTrait、SchemaManager,可选内嵌 CLI | src/ 2 388 |
sea-orm-cli | 2.0.4 | 命令行 sea-orm-cli:从数据库生成实体、跑迁移 | src/ 1 770 |
sea-orm-codegen | 2.0.4 | 把读到的表结构写成实体的 Rust 代码 | src/ 8 977 |
sea-schema | 0.18.1 | 读数据库现有结构(discovery),codegen 和 schema-sync 用 | src/ 9 390 |
sea-orm-sync | 2.0.4 | 同步版本,由脚本从 src/ 生成,见下文 | src/ 45 021,含测试和示例 69 823 |
sea-orm-arrow | 2.0.0-rc.5 | Arrow / Parquet 集成(本卷不讲) | src/ 1 550 |
sea-orm-rocket | 0.6.0 | Rocket 集成(本卷不讲) | 649 |
行数是 find <dir> -name '*.rs' -print0 | xargs -0 cat | wc -l 在 tag 的解包上的结果,sea-query 的三个 crate 对的是 tag 1.0.2,sea-schema 对的是 crates.io 上的 0.18.1。
几条依赖关系值得记住:
- sea-query 不依赖 sqlx。 它的
[dependencies]全是可选的:sea-query-derive、sea-query-postgres-types,以及一串类型库(chrono、uuid、rust_decimal、serde_json……)。和驱动打交道的是单独的sea-query-sqlx,它依赖sea-query(开thread-safe)和可选的sqlx = "0.9"。sea-query 的 README 说「We provide integration for SQLx, postgres and rusqlite」,每一种都是这样一个独立的绑定 crate;sea-orm-sync用的就是另一个,sea-query-rusqlite。 - sea-orm-codegen 不依赖 sea-orm。 它只依赖
sea-query(用 sea-query 的类型描述列)和syn/quote/prettyplease,产出的是 Rust 源码文本;sea-orm 只出现在它的[dev-dependencies]里。 - sea-orm-cli 也不依赖 sea-orm。 它依赖 codegen、sea-schema 和 sqlx:连库读结构,交给 codegen 写文件。反过来,
sea-orm-migration的clifeature 依赖sea-orm-cli,迁移程序的命令行(up、down、fresh……)是从 cli 里拿的。 - sea-orm 的 sqlx 是可选的。
sqlx、sqlx-core、sea-query-sqlx都是optional = true,由sqlx-postgres这类 feature 打开。只用默认 feature(不开任何驱动)的 crate 照样能编译,find_by_id(1).build(DbBackend::Postgres)照样能打出 SQL,只是Database::connect("postgres://...")在运行时返回Err(Conn(Internal("The connection string 'postgres://lab:lab@192.168.64.106/lab' has no supporting driver.")))。
本站的 1.1.20 里,绑定 crate 叫 sea-query-binder(本站 Cargo.lock 里是 0.7.0),2.0 换成了 sea-query-sqlx。
sea-orm 的 src/:每个目录回答一个问题
src/lib.rs 有 740 行,其中 674 行是 //! 文档(和 README 一样的内容),代码只是声明模块、pub use 一遍:database、driver、entity、error、executor、query、schema 都被整体重新导出,所以用户写 sea_orm::Select、sea_orm::DatabaseConnection,看不出它们在哪个目录。
| 目录 | 行数 | 回答的问题 | 主要文件(行数) | 讲它的部分 |
|---|---|---|---|---|
entity/ | 11 442 | 一张表在 Rust 里是什么 | active_model.rs 2 586、base_entity.rs 1 288、column.rs 1 200、column_def.rs 699、active_value.rs 642、relation.rs 604、active_enum.rs 598、active_model_ex.rs 467、compound/(HasOne / HasMany / BelongsTo)、registry.rs | 02 实体、05 写入 |
query/ | 6 793 | 怎么构造一条语句 | loader.rs 1 864、helper.rs 922(QuerySelect、QueryFilter)、insert.rs 910、join.rs 905、select.rs 408、update.rs 365、delete.rs 314 | 04 查询、05 写入 |
executor/ | 10 951 | 怎么执行,结果怎么变回 Rust 值 | cursor.rs 2 499、query.rs 1 902(QueryResult、TryGetable)、consolidate.rs 1 438、select.rs 1 223、paginator.rs 882、insert.rs 610、update.rs 427 | 04 查询、05 写入 |
database/ | 5 690 | 连接、事务、mock 是什么 | db_connection.rs 926、mock.rs 886、transaction.rs 816、mod.rs 752(Database::connect、ConnectOptions)、restricted_connection.rs 602、connection.rs 250(ConnectionTrait) | 06 连接与事务 |
driver/ | 3 482 | 每种驱动怎么接上 | sqlx_postgres.rs 951、rusqlite.rs 749、sqlx_mysql.rs 551、sqlx_sqlite.rs 514、mock.rs 282、sqlx_common.rs 183(ConnectOptions 到 sqlx 池参数)、proxy.rs 160 | 绑到 sqlx、06 连接与事务 |
schema/ | 1 848 | 怎么从实体建表、怎么同步结构 | builder.rs 686、entity.rs 507、topology.rs 432 | 07 迁移与 schema |
rbac/ | 1 889 | 按用户和角色限制能执行哪些语句 | engine/mod.rs 837、context.rs 322 | 本卷不讲 |
dynamic/ | 532 | 编译期不知道表结构时怎么用 | 模块文档写着「Unstable. The API in this module may change between minor versions.」 | 本卷不讲 |
value/ | 310 | 几个额外的值类型 | text_uuid.rs(存成 TEXT 的 UUID)、时间戳包装 | 列与值 |
tests_cfg/ | 1 191 | 上游测试和文档示例用的实体(cake、fruit、filling……) | cake_expanded.rs 是手写的「宏展开」版本,和宏的实际输出不保证一致 | — |
顶层还有 error.rs(349 行,DbErr,见错误)、docs.rs(173 行,四条设计原则的文档)、value.rs、metric.rs、util.rs(debug_print!)。
一条查询主线横穿其中四个目录:entity/base_entity.rs(find_by_id)→ query/select.rs(Select)→ executor/select.rs(Selector::one)→ database/connection.rs 和 db_connection.rs(query_one)→ driver/sqlx_postgres.rs(acquire + fetch_one)→ executor/query.rs(QueryResult)。这条线的逐步拆解见一次 find 的全过程。
sea-orm-macros/src/ 的 1.3 万行主要在 derives/ 下,每个派生一个文件:entity_model.rs、model_ex.rs(#[sea_orm::model])、active_model.rs、active_model_ex.rs、relation.rs、active_enum.rs、value_type.rs、partial_model.rs、from_query_result.rs、entity_loader.rs 等,见 DeriveEntityModel 与 dense 格式。sea-query 的 src/ 里,本卷用到的是 query/(8 237 行,各种语句)、expr/(2 624)、value/(2 152)、backend/(5 952,其中 query_builder.rs 2 114、postgres/ 1 566)和 prepare.rs(220),见 03 sea-query。
feature 体系
根 Cargo.toml 的 features 表大致分成下面几组。每个 feature 再用 crate/feature 或 crate?/feature 的写法转发给 sea-query、sea-query-sqlx、sea-orm-macros、sea-schema。
| 组 | feature | 说明 |
|---|---|---|
| 默认 | macros、with-json、with-chrono、with-rust_decimal、with-uuid、with-time、sqlite-use-returning-for-3_35、stream | 默认集里没有驱动,也没有运行时,两样都要自己开 |
| 驱动 | sqlx-postgres、sqlx-mysql、sqlx-sqlite、sqlx-all | sqlx-postgres = sqlx-dep(打开 sqlx、sqlx-core)+ sea-query-sqlx/sqlx-postgres + sea-orm-macros/sqlx-postgres + postgres-array + sea-schema?/sqlx-postgres |
| 运行时与 TLS | runtime-tokio、runtime-tokio-rustls、runtime-tokio-native-tls、runtime-async-std 及其两个 TLS 组合 | runtime-tokio-rustls = sqlx?/tls-rustls + runtime-tokio |
| 类型 | with-json、with-chrono、with-time、with-uuid、with-rust_decimal、with-bigdecimal、with-ipnetwork、with-mac_address、postgres-array、postgres-vector、with-arrow | 同时打开 sea-query 的同名 feature 和 sea-query-sqlx 的绑定 |
| 功能 | mock、proxy、debug-print、tracing-spans、schema-sync、entity-registry、rbac、seaography、serde、stream | 见下 |
| 行为开关 | postgres-use-serial-pk、sqlite-use-returning-for-3_35、mariadb-use-returning、sqlite-no-row-value-before-3_15 | 改变生成的 SQL |
功能那一组里,有的只是一个 cfg 开关,没有任何依赖:mock = []、debug-print = []、tracing-spans = []。它们在源码里各自打开一段 #[cfg(feature = "...")] 代码:mock 给 DatabaseConnectionType 加一个 MockDatabaseConnection 分支(这会改变这个 enum 的形状,见 Mock 与 Proxy),debug-print 让驱动里的 debug_print! 展开成 tracing::debug!,tracing-spans 让 with_db_span! 为每条语句建一个 span。另一些会拉进依赖:schema-sync = ["sea-schema"],entity-registry = ["inventory", "sea-orm-macros/entity-registry"](用 inventory 在链接期收集所有实体),rbac = ["sea-query/audit", "macros"](sea-query 的 audit 能列出一条语句访问了哪些表),stream = ["dep:ouroboros"](stream() 返回的流要同时持有连接和借用它的查询,用 ouroboros 做自引用结构)。
运行时和 TLS 在 sea-orm 里还是组合 feature(runtime-tokio-rustls),而 sqlx 0.9 已经把这类组合 feature 删掉了(见 sqlx 卷的源码地图)。sea-orm 这边是自己把组合拆开再转发:runtime-tokio-rustls 打开 sqlx?/tls-rustls 和 runtime-tokio,后者再打开 sqlx?/runtime-tokio。tls-rustls 在 sqlx 0.9 里是 tls-rustls-ring-webpki 的别名。本卷实验 crate 开的是 sqlx-postgres、runtime-tokio-rustls、macros、with-chrono、with-json,没有关默认 feature,所以默认集也在。
sea-orm-sync:用 sed 生成的同步版本
sea-orm-sync 是 sea-orm 的同步 API:没有 async、没有 .await,不需要异步运行时。它的 Cargo.toml 写着 name = "sea-orm-sync"、description = "🐚 The sync version of SeaORM",[lib] 的 name = "sea_orm",所以用户代码里照样写 use sea_orm::...。它的驱动只有 rusqlite:rusqlite = ["sea-query-rusqlite/sea-orm", "sea-schema-sync/rusqlite"],而 sqlx-postgres、sqlx-mysql、sqlx-sqlite、sqlx-dep 在这里都是空 feature,源码里的 driver/sqlx_postgres.rs 虽然也被复制了过来,却没有依赖能让它编译。README 的说法是「making it ideal for lightweight CLI programs with SQLite」。
这四万五千行不是手写的。build-tools/make-sync.sh 先删掉 sea-orm-sync/src 和 sea-orm-sync/tests,把 src、tests 和快速入门示例整个复制过去,然后对每个 .rs 文件跑六十多条 sed 替换,最后 cargo +nightly fmt。替换的核心几条:
replace_rs 's/Box::pin(async move {/({/' src
replace_rs 's/AsyncFnOnce/FnOnce/g' src
replace_rs 's/transaction_async/transaction/g' src tests
replace_rs 's/async //' src
replace_rs 's/\.await//' src
replace_rs '/#\[async_trait::async_trait/d' src
replace_rs 's/#\[tokio::test\]/#\[test\]/' src
replace_rs "s/type Stream<'a>: Stream/type Stream<'a>: Iterator/" src
replace_rs 's/ + Send//' src
replace_rs 's/Box::pin/Box::new/' src
replace_rs 's/impl Stream</impl Iterator</' src
replace_rs 's/use futures_util::lock::Mutex;/use std::sync::Mutex;/' src
也就是:把所有 async 和 .await 删掉,async_trait 属性删掉,Stream 换成 Iterator,Send / Sync 约束删掉,futures_util 的异步锁换成 std::sync::Mutex。这能行得通,靠的是 src/ 的写法配合脚本:异步代码里不出现「删掉 async 就不成立」的写法,脚本没法机械处理的地方,在源码里用 #[cfg(feature = "sync")] 分开写:src/ 里有 57 处,例如 database/tracing_spans.rs 的 with_db_span! 在同步版本里用 span.in_scope(..),异步版本里用 Instrument::instrument(..).await。
实验:重跑脚本,和发布的内容比较
在 tag 解包的一个副本里(src、tests、sea-orm-sync、sea-orm-macros、sea-orm-arrow、build-tools、.rustfmt.toml 和 examples/quickstart/src/main.rs,后两个 crate 是 cargo fmt 解析 sea-orm-sync 的工作区时要读的路径依赖),先把随 tag 发布的 sea-orm-sync 另存一份,再运行脚本:
cp -r sea-orm-sync shipped-sync
bash build-tools/make-sync.sh; echo "make-sync exit=$?"
for d in src tests examples; do
diff -r shipped-sync/$d sea-orm-sync/$d > /dev/null && echo "$d: identical" || echo "$d: differs"
done
grep -rc "async fn" src | awk -F: '{s+=$2} END {print "async fn in src:", s}'
grep -rc "async fn" sea-orm-sync/src | awk -F: '{s+=$2} END {print "async fn in sync src:", s}'
M2 Max、macOS(脚本开头会检测 GNU / BSD sed,macOS 上用 sed -i '')上的输出:
make-sync exit=0
src: identical
tests: identical
examples: identical
async fn in src: 512
async fn in sync src: 0
重新生成的 src、tests、examples 和 tag 里发布的逐字节相同;src/ 里 512 行含 async fn,生成的版本里一行也没有。两边都是 145 个 .rs 文件,行数从 45 625 变成 45 021,少掉的是被删的 #[async_trait::async_trait]、use futures_util::... 这类行,再加上 rustfmt 把变短的签名合并成一行。
也就是说,2.0.4 的同步版本确实就是这个脚本的产物。仓库的 CI 只在 sea-orm-sync 目录里跑 cargo test --test '*' --features tests-features,rusqlite 和两个示例,并不重跑脚本比较差异:两份代码一致,靠的是改了 src/ 的人记得重新生成。
make-sync.sh、sea-orm-sync/、src/driver/rusqlite.rs 是同一个提交加进仓库的:PR #2847「Rusqlite Support」(2025-12-13 合并),提交信息依次是「Init sea-orm-sync」「Stream and transaction」「All tests pass」「CI」「Support sea-schema-sync」。在这之前 sea-orm 只有异步 API,驱动都是 sqlx 的。PR 的说明只有一句「Almost, since sea-schema is still missing. But the core ORM works」,读结构用的 sea-schema-sync 是后来补上的。CHANGELOG 把它记在 2.0.0-rc.21 的 New Features 里:「New sea-orm-sync crate for synchronous SQLite via rusqlite」,同一节还写着「Stream and transaction support」「Integration with sea-schema-sync」。现在 src/driver/rusqlite.rs 放在异步的 src/ 里,但异步 crate 的 rusqlite = [] 不带任何依赖,文件里用的 sea_query_rusqlite(连同它重新导出的 rusqlite)只在同步版本的 Cargo.toml 里才有;脚本的倒数第二步还专门对这个文件做了一次反向替换(try_lock().ok() 改回 try_lock())。
小结
- 主线上的 crate:
sea-orm(运行时)、sea-orm-macros(派生和#[sea_orm::model])、sea-query(语句树和方言)、sea-query-sqlx(绑参数)、sqlx(执行);迁移和代码生成是sea-orm-migration、sea-orm-cli+sea-orm-codegen+sea-schema。 - sea-query 不依赖 sqlx,绑定在单独的 crate 里;sea-orm 的 sqlx 依赖是可选的,由驱动 feature 打开。
src/按问题分目录:entity/是表,query/造语句,executor/执行并解码,database/是连接和事务,driver/接驱动,schema/建表和同步。- 默认 feature 里没有驱动和运行时;
mock、debug-print、tracing-spans是纯cfg开关,mock会改变DatabaseConnectionType的形状。 sea-orm-sync由build-tools/make-sync.sh用 sed 从src/生成,在 2.0.4 上重跑脚本,结果和发布的逐字节相同;只有 rusqlite 驱动。