Rust in Everything
基于 sqlx 0.9.0

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_patherror 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
📝NOTE
冷知识:0.9 的宏会读上级目录的 .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 之后)。

💬 关联讨论

发起讨论
加载中...