SQLx CLI(sqlx-cli)は、RustのSQLxクレートに付属するコマンドラインツールです。データベースの作成、マイグレーションの作成と適用、そしてデータベースに接続せずにビルドするためのcargo sqlx prepareを受け持ちます。この記事では、現行版のsqlx-cli 0.9.0を検証環境(macOS・Rust 1.99.1・PostgreSQL 18.6)でビルドし、各コマンドを実行したときの出力をそのまま載せています。同じ名前のGoのjmoiron/sqlxは別のライブラリで、CLIもマイグレーション機能も持たないため、この記事の対象外です。
まとめ:sqlx-cli 0.9.0のコマンド早見と押さえる3点
| コマンド | 用途 |
|---|---|
| cargo install sqlx-cli | CLI本体の導入 |
| sqlx database create / drop | DATABASE_URLのDBを作成・削除 |
| sqlx database setup / reset | 作成+全マイグレーション適用/作り直し |
| sqlx migrate add -r 名前 | up・downの対でファイルを作成 |
| sqlx migrate run / info / revert | 適用・状態確認・1つ巻き戻し |
| sqlx migrate build-script | migrate!用のbuild.rsを生成 |
| cargo sqlx prepare | .sqlxにクエリ情報を保存 |
cargo sqlx prepare --check |
キャッシュ不足・内容不一致でexit 1 |
- 適用済みのマイグレーションファイルは書き換えない。書き換えると
sqlx migrate runがwas previously applied but has been modifiedで止まり、終了コード1を返します。 query!系マクロを使うなら.sqlxをコミットし、CIでcargo sqlx prepare --checkを回す。DBの無いビルド環境でもSQLX_OFFLINE=trueでコンパイルできます。- 0.9.0では
--lockedを前提にしない。公式のCHANGELOGがリポジトリでのCargo.lock管理をやめたと告知しており、検証環境では--lockedで取り下げ済み(yanked)の依存4件の警告が出ました。
sqlx-cliのインストールと接続設定
cargo installとfeatureの選び方
sqlx-cliはcargo installで入れます。Rust自体の導入やcargo installの仕組みはCargo(Rust)とは?インストールとcargo installの使い方・主要コマンド一覧にまとめています。0.9.0の動作に必要なRustは1.94.0以上です(crates.ioのrust-version)。
cargo install sqlx-cli
既定のfeatureはpostgres・sqlite・mysql・native-tls・completions・sqlx-tomlです(sqlx-cliのCargo.toml)。全部入りのビルドは検証環境で3分58秒かかり、sqlxとcargo-sqlxの2つのバイナリが入りました。使うDBが1つなら、既定を切って絞るとビルドが軽くなります。
cargo install sqlx-cli --no-default-features --features rustls,postgres
postgresの代わりにmysql・sqliteを指定でき、カンマ区切りで併記もできます。OS側のSQLiteライブラリを使うならsqlite-unbundled、OpenSSLをソースからビルドするならopenssl-vendoredを選びます。0.9.0で追加されたmysql-rsaは、TLSを使わずにcaching_sha2_passwordまたはsha256_password認証でRSA鍵交換を必要とするMySQL・MariaDBへ接続する場合に必要です(sqlx-cli README)。導入後はsqlx --versionでsqlx-cli 0.9.0と表示されることを確かめ、アプリ側のsqlxクレートと版を揃えます。
sqlx-cli 0.9.0で--lockedを付けない理由
0.8系までの解説ではcargo install sqlx-cli --lockedがよく勧められていました。0.9.0のCHANGELOGは、リポジトリでのCargo.lockの追跡をやめたことを告知し、cargo install --locked sqlx-cliは今後機能しなくなると書いています。再現性のあるビルドが必要なら自前でロックファイルを持つよう案内しています。
検証環境で--locked付きのインストールを実行すると、ビルドそのものは成功しましたが、次の警告が4件(chacha20 0.10.0、crypto-bigint 0.7.3、der 0.8.0、spin 0.9.8)出ました。
warning: package `chacha20 v0.10.0` in Cargo.lock is yanked in registry `crates-io`
|
= help: consider running without --locked
取り下げ済みの版で固定されるため、0.9.0では--lockedを付けない通常のcargo install sqlx-cliを使うのが公式の案内に沿った入れ方です。CIで版を固定したいときは--version 0.9.0で版番号を指定します。
DATABASE_URLと.envの読み込み
ほぼすべてのコマンドが接続先を必要とします。指定方法は-D(--database-url)オプション、環境変数DATABASE_URL、カレントディレクトリの.envの3通りです。どれも無いと次のエラーで終了コード1になります。
$ sqlx migrate info
error: `--database-url` or `DATABASE_URL` must be set
DATABASE_URL=postgres://postgres@localhost:5432/app
.envを読むのはsqlx-cliと、コンパイル時にスキーマを問い合わせるquery!系マクロです。ビルドしたアプリが実行時に.envを読むわけではありません。検証でstd::env::var("DATABASE_URL")を使うアプリを.envだけ置いてcargo runすると、Error: NotPresentで終了しました。実行時にも.envを使うならdotenvyクレートで読み込むか、環境変数を渡します。CLIに.envを読ませたくない場面では--no-dotenvを付けます。
SQLxクレートの依存追加とfeature設定
アプリからSQLを実行するには、CLIとは別にsqlxクレートを依存に加えます。
cargo add sqlx@=0.9.0 --features runtime-tokio,tls-rustls,postgres
cargo add tokio --features full
0.9.0の既定featureはany・macros・migrate・jsonで、query!マクロとmigrate!マクロは追加指定なしで使えます。オフラインモード用のoffline機能はmacrosに含まれるため、古い記事にあるfeatures = ["offline"]の追記は不要です。ランタイムとTLSはruntime-tokioとtls-rustlsのように別々に指定します。runtime-tokio-rustlsのような結合名は0.9.0で削除されました。tls-rustlsは暗号にring、ルート証明書に同梱のwebpkiを使う構成の別名で、AWS-LCを使うならtls-rustls-aws-lc-rs、OSの証明書ストアを使うならtls-rustls-ring-native-rootsを選びます。SQLxは非同期APIなので#[tokio::main]などのランタイムが要ります。非同期の基礎はRustのasync/await入門|Futureとpollの仕組み、Tokioでの実行とよくあるエラーで確認できます。
sqlx migrateによるマイグレーション管理
sqlx databaseによるDBの作成・再作成
sqlx database createはDATABASE_URLのDBを作り、成功しても何も表示しません。setupは作成と未適用マイグレーションの適用を一度に行い、resetは削除・再作成・適用までを行います。
$ sqlx database setup
Applied 20261027053727/migrate create users (23.063217ms)
Applied 20261027053737/migrate add email (1.015279ms)
dropとresetは確認のプロンプトを出します。端末の無いCIで-yを付けずに実行すると、Confirm dialog failed with IO error: not a terminalでパニックし、終了コード101になりました。CIではsqlx database reset -yのように必ず-yを付けます。SQLiteでは-D sqlite:app.dbのように指定すると、createがファイルを作ります。
sqlx migrate addによるファイル生成と採番
-rを付けると、同じ版番号の.up.sql(適用用)と.down.sql(巻き戻し用)が対で作られます。
$ sqlx migrate add -r create_users
Creating migrations/20261027053727_create_users.up.sql
Creating migrations/20261027053727_create_users.down.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL
);
down側にはDROP TABLE users;を書きます。-rを付けないaddは、直前のマイグレーションの形式に合わせます。最新が可逆なら、sqlx migrate add add_emailでも.up.sqlと.down.sqlの対が作られました。前進のみの.sql1本にしたいときは--simpleを付けます。版番号は既定でYYYYMMDDHHMMSSのタイムスタンプです。-s(--sequential)を付けると1・2・3の連番になり、直近2本の版番号の差が1なら連番が自動で続きます。
sqlx migrate run・info・revertの実行結果
infoは各マイグレーションの状態をpending(未適用)かinstalled(適用済み)で示します。run --dry-runは適用せずに対象だけを表示します。
$ sqlx migrate info
20261027053727/pending create users
$ sqlx migrate run --dry-run
Can apply 20261027053727/migrate create users (0ns)
$ sqlx migrate run
Applied 20261027053727/migrate create users (24.70484ms)
$ sqlx migrate info
20261027053727/installed create users
revertは最後の1本だけを戻します。全部を戻すなら--target-version 0を指定し、新しい順に巻き戻されます。runにも--target-versionがあり、指定した版まで適用して止められます。
$ sqlx migrate revert
Applied 20261027053737/revert add email (2.172936ms)
# 以下はcreate_usersとadd_emailを両方適用済みの状態から実行した別の例
$ sqlx migrate revert --target-version 0
Applied 20261027053737/revert add email (14.183771ms)
Applied 20261027053727/revert create users (45.430415ms)
マイグレーションの置き場所をmigrations/以外にするなら--source ../db/migrationsのように指定します。0.9.0ではsqlx migrate override skip(未適用分を実行せずに適用済みとして記録する)も加わりましたが、ヘルプ自体が「potentially dangerous」と注記しているため、既存DBに後からSQLxを入れる場合など目的がはっきりしているときだけ使います。
_sqlx_migrationsテーブルに記録される内容
適用履歴は_sqlx_migrationsテーブルに残ります。PostgreSQL 18.6で列を確認した結果は次のとおりです。
| 列 | 型 | 中身 |
|---|---|---|
| version | bigint(主キー) | ファイル名先頭の版番号 |
| description | text | 名前(_を空白に置換) |
| installed_on | timestamptz | 適用日時 |
| success | boolean | 成功したか |
| checksum | bytea | ファイル内容のハッシュ(48バイト) |
| execution_time | bigint | 実行時間(ナノ秒) |
実測ではexecution_timeが24704840で、CLIの表示(24.70484ms)とナノ秒単位で一致しました。checksumは48バイトで、この値をローカルのファイルと突き合わせるのが次の節の挙動です。複数のクレートが同じDBにマイグレーションを流す場合は、0.9.0の設定ファイルsqlx.tomlでこのテーブル名を変えられます。
適用済みマイグレーションの改変によるrunの停止
適用済みのadd_email.up.sqlの末尾にコメントを1行足しただけで、infoはチェックサムの不一致を表示し、runはエラーで止まりました。
$ sqlx migrate info
20261027053727/installed create users
20261027053737/installed (different checksum) add email
applied migration had checksum 4b886657d12acaaa416c4a98503159bf...
local migration has checksum a4593037a48a4c2c722779838978b41e...
$ sqlx migrate run
error: migration 20261027053737 was previously applied but has been modified
$ echo $?
1
書き換えた内容が黙って捨てられるのではなく、後続の未適用分も含めてrun全体が進まなくなります。共有DBや本番に適用済みのファイルは書き換えず、変更は新しいマイグレーションとして追加します。手元の開発DBだけなら、revertで戻してから直してrunし直すか、sqlx database reset -yで作り直せば解消します。git pullで他人が書き換えたファイルを取り込んだときにもこのエラーが出るので、レビューで「適用済みファイルの差分」を止めるのが確実です。
sqlx::migrate!による起動時のマイグレーション適用
CLIで手動適用する代わりに、migrations/をバイナリに埋め込み、起動時に適用する方法もあります。migrate!マクロは既定featureのmigrateで使えます。
sqlx::migrate!().run(&pool).await?;
埋め込みはコンパイル時に行われるため、migrations/にファイルを足しただけでは再ビルドされないことがあります。sqlx migrate build-scriptを実行すると、変更を検知するbuild.rsが生成されます。
$ sqlx migrate build-script
Created `build.rs`; be sure to check it into version control!
// generated by `sqlx migrate build-script`
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations");
}
使い分けの基準は、マイグレーションを誰がいつ流すかです。アプリ1つがDBを専有し、起動とスキーマ更新を一体で扱いたいならmigrate!が向きます。複数のインスタンスが同時に起動する構成や、デプロイ前にDBA・レビュー担当が適用を確認する運用では、CIやデプロイ手順の中でsqlx migrate runを明示的に実行する方が事故を追いやすくなります。
cargo sqlx prepareによるオフラインビルドの準備
query!・query_as!マクロは、ビルド時に実DBへ接続してSQLの構文・列の型・NULL可否を検証します。そのままではDBの無いCIやDockerビルドでコンパイルが通りません。cargo sqlx prepareは検証結果を.sqlxディレクトリに保存し、DBなしのビルドを可能にします。
.sqlxに書き出される中身
$ cargo sqlx prepare
query data written to .sqlx in the current directory; please check this into version control
検証用アプリにはquery!系の呼び出しが4か所ありましたが、生成されたファイルは3つでした。ファイル名はSQL文字列のハッシュで、同じSQLを2か所で使うと1ファイルにまとまります。SELECTのファイルの要部は次のとおりで、列の型(Int8・Text)、パラメータの型、nullableが記録されます。
{
"db_name": "PostgreSQL",
"query": "SELECT id, name FROM users WHERE id = $1",
"describe": {
"columns": [
{ "ordinal": 0, "name": "id", "type_info": "Int8" },
{ "ordinal": 1, "name": "name", "type_info": "Text" }
],
"parameters": { "Left": ["Int8"] },
"nullable": [false, false]
},
"hash": "220beaa3eb440cac34825e073d8fe3cf285373fec79ef1919a9440a0e235988c"
}
ワークスペースで複数のクレートがマクロを使うならcargo sqlx prepare --workspaceでルートに1つの.sqlxを作ります。テストコードやfeatureの裏にあるクエリは、通常のビルドに入らないため保存されません。cargo sqlx prepare -- --all-targets --all-featuresのように、--の後ろにcargoへの引数を渡して対象に含めます。
SQLX_OFFLINEとDATABASE_URLの優先順位
ビルド時のマクロは、DATABASE_URLがあればDBに接続し、無ければ.sqlxを読みます。DATABASE_URLが.sqlxより優先されるため、.envが残ったCIではDBへ接続しにいって失敗します。SQLX_OFFLINE=true(または1)を設定すると、DATABASE_URLがあっても.sqlxだけを読みます。.sqlxに無いクエリをSQLX_OFFLINE=trueでビルドしたときのエラーは次のとおりで、cargo buildは終了コード101で止まりました。
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`
--> src/main.rs:29:16
CIでのprepare --checkによるキャッシュ検証
--checkは.sqlxを書き換えずに、現在のクエリとスキーマに対して古くなっていないかを判定します。SQLを1か所変えてから実行すると、終了コード1で次のように失敗しました。逆に、コードから消したクエリのキャッシュが.sqlxに残っているだけなら、warning: potentially unused queries found in .sqlxの警告が出るだけで終了コードは0でした。
$ cargo sqlx prepare --check
error: prepare check failed: .sqlx is missing one or more queries; you should re-run sqlx prepare
CIでは、テスト用のDBを立ててマイグレーションを適用した後にcargo sqlx prepare --checkを実行し、本番イメージのビルドはSQLX_OFFLINE=trueで行う、という2段構えにすると「prepareのやり忘れ」をマージ前に止められます。この構成はAxumなどでWebバックエンドをコンテナ化するときに効きます(Axum(アクサム)とは?Rust製Webフレームワークの使い方・テスト・Actix Web比較【axum 0.8.9】)。
SQLxでクエリを書く:query!・query_as!・トランザクション
次のコードはsqlx 0.9.0とPostgreSQL 18.6でビルド・実行し、inserted id = 1、User { id: 1, name: "Alice" }、committed, last id = 3と出力されたものです。ビルド前に、検証用DBへsqlx migrate runでスキーマを適用するか、対応する.sqlxを用意します。実行時のmigrate!だけでは、コンパイル時のquery!系マクロの検証を満たせません。
use sqlx::postgres::PgPoolOptions;
use sqlx::{Postgres, Transaction};
#[derive(Debug)]
struct User {
id: i64,
name: String,
}
async fn insert_in_tx(tx: &mut Transaction<'_, Postgres>, name: &str) -> Result<i64, sqlx::Error> {
let rec = sqlx::query!("INSERT INTO users (name) VALUES ($1) RETURNING id", name)
.fetch_one(&mut **tx)
.await?;
Ok(rec.id)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("DATABASE_URL")?;
let pool = PgPoolOptions::new().max_connections(5).connect(&url).await?;
sqlx::migrate!().run(&pool).await?;
let rec = sqlx::query!("INSERT INTO users (name) VALUES ($1) RETURNING id", "Alice")
.fetch_one(&pool)
.await?;
println!("inserted id = {}", rec.id);
let user = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", rec.id)
.fetch_one(&pool)
.await?;
println!("{:?}", user);
let mut tx = pool.begin().await?;
sqlx::query!("INSERT INTO users (name) VALUES ($1)", "Bob")
.execute(&mut *tx)
.await?;
let id = insert_in_tx(&mut tx, "Carol").await?;
tx.commit().await?;
println!("committed, last id = {}", id);
Ok(())
}
- PostgreSQLで挿入した行のIDを受け取るには
RETURNINGとfetch_oneを使います。MySQLのlast_insert_id()に当たるメソッドはPostgreSQLにはありません。 query_as!の第1引数はマッピング先の型です。列名と構造体のフィールド名が一致している必要があります。- トランザクションは、所有している
txなら&mut *tx、&mut Transactionとして受け取った関数内では&mut **txを実行対象に渡します。0.6系までの&mut txはコンパイルできません。
マクロを使わないsqlx::query("...")の引数は、0.9.0からimpl SqlSafeStrになり、通常の文字列は&'static strとして渡す必要があります。format!で組み立てたStringを渡すと、error[E0277]: dynamic SQL strings should be audited for possible injectionsでコンパイルが止まります(検証環境で確認)。文字列連結によるSQLインジェクションをレビューで見つけやすくするための変更です。動的な値にはバインドパラメータやQueryBuilder::push_bindを使います。動的SQL文字列を直接渡す必要がある場合は、SQLインジェクションが起きないことを確認したうえでsqlx::AssertSqlSafe(sql)で包みます。このラッパー自体は入力の検証やエスケープを行いません。
SQLiteでも同じAPIで書けます。ただしSqliteConnectOptionsの既定はcreate_if_missing: falseで、ファイルが無いと接続に失敗します。アプリ側でファイルを作らせるなら接続文字列をsqlite://app.db?mode=rwcにするか、create_if_missing(true)を指定します。テスト用ならsqlite::memory:が使えます。
SQLxとsqlx-cliの採用判断
SQLx本体が対応するDBかどうかと、CLIに任せる処理を分けて判断します。
- SQL Serverを使うなら採用しない:SQLxの対応はPostgreSQL・MySQL・MariaDB・SQLiteで、MSSQLドライバーは0.7.0で削除されました。
- 動的SQLが中心なら
prepareは限定的:prepareとオフラインモードが効くのはquery!系マクロのコンパイル時検証だけです。検索条件をQueryBuilderで組み立てるクエリが大半ならprepareの対象は限られますが、CLIによるマイグレーション管理はそのまま使えます。query!系マクロを使わない構成では.sqlxの管理自体が不要です。 revertだけを本番の復旧手順にしない:revertはdown側のSQLを流すだけで、up側で消したデータは戻りません(migrate add --helpにも明記)。列の追加のような可逆な変更はdown SQLで戻し、列やテーブルの削除などデータを失う変更は、バックアップからの復元か、修正用の新しいマイグレーションを流す前進修正で復旧する手順を別に用意します。
既存システムのDB設計やマイグレーション計画から相談したい場合は、データベース設計・移行支援も参考にしてください。
よくある質問
sqlx-cliとSQLx本体はどう違いますか?
SQLx本体はアプリからSQLを実行するクレートで、sqlx-cliはDBの作成、マイグレーション、.sqlxの生成を行うコマンドです。query!マクロやmigrate!マクロはクレート側の機能なので、CLIが無くても使えます。CLIが必要になるのは、マイグレーションファイルを作る・手動で適用する・オフラインビルド用にprepareするときです。
cargo install sqlx-cliが遅い・失敗するときは?
既定では3種類のDBドライバーとnative-tlsをすべてビルドするため、検証環境で約4分かかりました。--no-default-features --features rustls,postgresのように使うDBへ絞ると短くなります。OpenSSLが見つからずに失敗する場合はrustlsを選ぶかopenssl-vendoredを付けます。0.9.0はRust 1.94.0以上が必要なので、rustup updateも確認してください。
sqlx migrate runがエラーで止まるのはなぜですか?
was previously applied but has been modifiedなら、適用済みファイルの中身が変わり、_sqlx_migrationsのチェックサムと一致しなくなっています。sqlx migrate infoで(different checksum)の付いた版を探し、ファイルを元に戻して変更は新しいマイグレーションに移します。開発DBならsqlx database reset -yで作り直しても解消します。
Goのsqlx(jmoiron/sqlx)とRustのSQLxは同じものですか?
別物です。Goのjmoiron/sqlxは標準のdatabase/sqlに構造体へのスキャンなどを足すライブラリで、CLI・マイグレーション・コンパイル時検証は持ちません。sqlx-cliはRustのSQLx専用です。
sqlx-cliはSQLiteでも使えますか?
使えます。DATABASE_URL=sqlite:app.dbとしてsqlx database createを実行するとファイルが作られ、以降のmigrateやprepareはPostgreSQLと同じ手順です。sqlx-cliが作るSQLiteのDBは既定でWALモードになり、アプリ側で別のjournal_modeを指定するとdatabase is lockedの原因になるとヘルプに注記があります。