set DATABASE_URL to use query macros online, or run cargo sqlx prepare to update the query cache
主线位置:编译期检查主线的第一步。
query!展开时先决定数据从哪来:连库(DATABASE_URL)还是读缓存(.sqlx/query-<hash>.json)。两条路都走不通时报这句。数据源怎么选、.sqlx怎么写出来、sqlx.toml能改哪些名字,见离线模式与 sqlx.toml;本页讲构建失败时怎么判断是哪种情况,以及缓存过期这个不报错的坑。
flowchart TB
start["query! 展开"] --> off{"SQLX_OFFLINE 为真"}
off -->|否| url{"有 DATABASE_URL"}
url -->|有| live["编译时连库"]
url -->|没有| cache
off -->|是| cache{"有 query-hash.json"}
cache -->|有| cached["读缓存"]
cache -->|"没有, 未设离线"| e1["set DATABASE_URL ..."]
cache -->|"没有, 设了离线"| e2["SQLX_OFFLINE=true but ..."]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0",cargo sqlx 是同版本的 sqlx-cli。实验 crate 是一个只有 src/main.rs 的二进制 crate,不在 workspace 里:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
let rows = sqlx::query!("select id, name from item order by id").fetch_all(&pool).await?;
for r in rows {
println!("{} {}", r.id, r.name);
}
Ok(())
}
现象
没有 DATABASE_URL、也没有 .sqlx 目录时 cargo build:
error: set `DATABASE_URL` to use query macros online, or run `cargo sqlx prepare` to update the query cache
--> src/main.rs:4:14
|
4 | let rows = sqlx::query!("select id, name from item order by id").fetch_all(&pool).await?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `$crate::sqlx_macros::expand_query` which comes from the expansion of the macro `sqlx::query` (in Nightly builds, run with -Z macro-backtrace for more info)
error: could not compile `pit1-offline` (bin "pit1-offline") due to 1 previous error
设了 SQLX_OFFLINE=true(不管有没有 DATABASE_URL)、缓存里没有这条:
error: `SQLX_OFFLINE=true` but there is no cached data for this query, run `cargo sqlx prepare` to update the query cache or unset `SQLX_OFFLINE`
有 .sqlx 也可能报这两句:缓存文件按 SQL 文本的 SHA-256 命名,改了 SQL(哪怕只是 order by id 改成 order by id desc)却没重新 prepare,新文本找不到对应的文件,报的是同样的两句话。
DATABASE_URL 设了但连不上,或者连上了但库不对,报的是另一类错误,位置一样在 query! 上:
| 情况 | 报错 |
|---|---|
| 服务端没起来(端口 1) | error communicating with database: Connection refused (os error 61) |
| 密码错 | error returned from database: password authentication failed for user "lab" at line 331 |
表在别的 schema,URL 没设 search_path | error returned from database: relation "item" does not exist at line 1449 |
后两行末尾的 at line N 是服务端 C 源码的行号,不是 SQL 的行号,见错误的「0.9 的 at line N」。
出处
expand_input 一开头就分两路:
let data_source = match &*metadata_env {
MacrosEnv {
offline: None | Some(false),
database_url: Some(db_url),
..
}
// Allow `DATABASE_URL=''`
if !db_url.is_empty() => QueryDataSource::live(db_url)?,
MacrosEnv {
offline,
offline_dir,
..
} => {
// Try load the cached query metadata file.
let filename = format!("query-{}.json", hash_string(&input.sql));
// Check SQLX_OFFLINE_DIR, then local .sqlx, then workspace .sqlx.
// ...
else {
return Err(
if offline.unwrap_or(false) {
"`SQLX_OFFLINE=true` but there is no cached data for this query, run `cargo sqlx prepare` to update the query cache or unset `SQLX_OFFLINE`"
} else {
"set `DATABASE_URL` to use query macros online, or run `cargo sqlx prepare` to update the query cache"
}.into()
);
};
- 只要
SQLX_OFFLINE不为真、DATABASE_URL非空,就一定连库,.sqlx不看。Dockerfile 里为运行时写了ENV DATABASE_URL=...,构建阶段就会去连那个地址,报上表第一行。 SQLX_OFFLINE为真(true,不分大小写,或1)时,DATABASE_URL被忽略,只读缓存。设SQLX_OFFLINE=true、DATABASE_URL指向连不上的地址,同一个 crate 照样编译成功。- 缓存按
SQLX_OFFLINE_DIR、crate 的.sqlx、workspace 根的.sqlx的顺序找,文件名是hash_string算的 SHA-256。 - 三个变量先读进程环境,没有时读
.env文件(load_env)。0.9 里读哪些.env变了,见下面的冷知识。
.sqlx 过期:编译通过,运行报错
缓存只按 SQL 文本查找,不知道库变了没有。先 cargo sqlx prepare 写出缓存,此时 name 是 not null,缓存里 "nullable": [false, false]。然后迁移把它改成可空,再插一行 NULL:
alter table item alter column name drop not null;
insert into item values (4, null, 1.00, '2026-10-24 11:00:00', null);
不设 DATABASE_URL(CI 的离线构建就是这样)重新编译:成功,name 仍是 String。运行:
Error: error occurred while decoding column 1: unexpected null; try decoding as an `Option`
Caused by:
unexpected null; try decoding as an `Option`
同样的代码设上 DATABASE_URL 在线编译,宏对着新表结构推断出 Option<String>,println!("{}", r.name) 编译不过:
error[E0277]: `Option<std::string::String>` doesn't implement `std::fmt::Display`
--> src/main.rs:6:29
|
6 | println!("{} {}", r.id, r.name);
| -- ^^^^^^ `Option<std::string::String>` cannot be formatted with the default formatter
这才是期望的结果:表变了,代码应该在编译时被拦下。离线构建读的是旧缓存,这一步被跳过了。cargo sqlx prepare --check 专门查这件事:重新生成一遍缓存,和 .sqlx 逐个比较(prepare_check)。为了让在线编译能通过,把那行改成 {:?} 再跑:
error: prepare check failed: one or more query files differ (query-c0e6ed9adc5f8a781795c0647388bb3814ba13d44eece983fc8381f6b82f6ced.json); you should re-run sqlx prepare
退出码是 1。重新 cargo sqlx prepare 之后,缓存里变成 "nullable": [false, true],离线构建的程序输出:
1 Some("pen")
2 Some("ink")
3 Some("pad")
4 None
.env,而且外层的说了算0.8.6 的宏只读一个 .env:crate 目录下有就用它,没有才退回 dotenvy::dotenv_iter()(从当前目录往上找第一个)。#4053「smarter .env loading, caching, and invalidation」(首次发布于 0.9.0)重写了这段,起因之一是被它取代的 #4018 发现 sqlx.toml 的 database_url_var「wasn't loaded from .env files」。新的 load_env 对 manifest_dir.ancestors() 的每一层都读 .env,后读到的覆盖先读到的,于是越靠外的目录优先级越高。
在 0.9.0 上验证:实验 crate 的 .env 写不带 search_path 的 URL,它的上一级目录的 .env 写带 search_path 的 URL,不设环境变量,编译成功;两个文件对调,报 relation "item" does not exist。0.8.6 下同样的布局用的是 crate 自己的 .env。上级目录放一个和 sqlx 无关、dotenvy 解析不了的 .env(一行 SMTP_PASSWORD=some space delimited password),即使环境变量里已经有 DATABASE_URL,0.9.0 也报:
error: error reading dotenv file "exp-sqlx-pit1/.env"
--> src/main.rs:4:14
...
0.8.6 下同样的布局编译通过。这是上游 #4276(「SQLx attempts to read unrelated parent directory .env」),修复 #4282 把查找限制在 workspace 之内,说明写的是「This reverts a behavior change to match previous releases」。它在 0.9.0 打 tag 之后合并,截至本页写作时还没有发布版本包含它;main 上的循环顺序没变,workspace 根的 .env 仍然覆盖成员 crate 的 .env(这一点只读了 main 的源码,没有运行)。
修法
- 本地开发:
DATABASE_URL指向一个跑过全部迁移的库,宏在线检查。 - CI 和 Docker 构建:把
.sqlx提交进仓库,构建时设SQLX_OFFLINE=true。这样即使镜像里为运行时设了DATABASE_URL,构建也不去连库。 - 防止缓存过期:CI 里另起一步,对一个跑过迁移的库执行
cargo sqlx prepare --check,非零退出就失败。每次改 SQL 或加迁移后,本地重新cargo sqlx prepare并提交。workspace 用cargo sqlx prepare --workspace。 - 在线编译报连接错误时,先用同一个 URL 在命令行连一次库(
psql "$DATABASE_URL"),确认地址、密码和search_path。
怎么确认没中招
git ls-files .sqlx有文件,数量和代码里query!系列宏的调用数量对得上(prepare --check会对多出来的文件打 warning)。- CI 的构建步骤里有
SQLX_OFFLINE=true,另有一步cargo sqlx prepare --check。 - 0.9.0 下检查 crate 目录到文件系统根的每一层有没有
.env:外层的同名变量会覆盖里层的,解析不了的.env会让构建失败。
小结
SQLX_OFFLINE不为真且DATABASE_URL非空时一定连库;否则按 SQL 的 SHA-256 找.sqlx/query-*.json,找不到就报这句(设了SQLX_OFFLINE=true时换一种说法)。- 改了 SQL 没重新
prepare,和完全没有缓存报的是同一句话。 .sqlx不知道表结构变了:缓存过期时离线构建照样通过,错误推迟到运行时。用cargo sqlx prepare --check在 CI 里拦。- 0.9.0 的宏读所有上级目录的
.env,越外层优先级越高,解析失败会让构建失败(#4276,修复 #4282 在 0.9.0 之后)。