これは、なにをしたくて書いたもの?
RustからSQLiteにアクセスしてみましょう、ということで。
RustからSQLiteにアクセスする
RustからSQLiteにアクセスするには、以下のクレートを使うとよさそうです。
違いが気になるところですが、Rusqliteを使う、でよさそうです。
Rusqliteクレート
Rusqliteクレートのドキュメントはこちら。
GitHubリポジトリーはこちら。
GitHub - rusqlite/rusqlite: Ergonomic bindings to SQLite for Rust · GitHub
現在のバージョンは0.39.0です。
https://docs.rs/rusqlite/0.39.0/rusqlite/
RusqliteはRustからSQLiteを使うためのラッパーです。rust-postgresのAPIをベースにしていたようですが、互換性があるわけでも
なさそうです。
サポートしているSQLiteのバージョンは3.34.1以上です。
Rusqlite / Usage / Supported SQLite Versions
フィーチャーフラグはいろいろありますが、内部にlibsqlite3-sysというクレートを持っており、これを合わせてビルドする
bundledをまずは使うとよさそうです。
rusqlite/libsqlite3-sys at v0.39.0 · rusqlite/rusqlite · GitHub
Rusqlite / Notes on building rusqlite and libsqlite3-sys
サンプルはドキュメントを見るか、
こちらのディレクトリーを参照するとよいでしょう。
https://github.com/rusqlite/rusqlite/tree/v0.39.0/examples
ちなみにasync/awaitには対応しておらず、その場合はこちらを使うようです。
それでは試してみます。
環境
今回の環境はこちら。
$ rustup --version rustup 1.29.0 (28d1352db 2026-03-05) info: This is the version for the rustup toolchain manager, not the rustc compiler. info: the currently active `rustc` version is `rustc 1.95.0 (59807616e 2026-04-14)`
準備
Cargoパッケージの作成。
$ cargo new --vcs none --lib rusqlite-getting-started Creating library `rusqlite-getting-started` package note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html $ cd rusqlite-getting-started
確認はテストコードで行うので、ライブラリークレートにしました。
Rusqliteクレートを使う
では、Rusqliteクレートを使っていきます。
依存関係の追加。
$ cargo add rusqlite --features bundled $ cargo add --dev uuid --features v4
Cargo.toml
[package] name = "rusqlite-getting-started" version = "0.1.0" edition = "2024" [dependencies] rusqlite = { version = "0.39.0", features = ["bundled"] } [dev-dependencies] uuid = { version = "1.23.1", features = ["v4"] }
uuidクレートはテスト用に使います。
Rusqliteのbundledというフィーチャーフラグですが、これを指定しない場合はその環境でSQLiteが利用できる必要があります。
bundledを指定すると、Rusqliteがこれを担います。
ビルド時にこういうエラーが発生した場合は、最初はbundledを指定しておくのがよいでしょう。
error: linking with `cc` failed: exit status: 1 | = note: "cc" "-m64" "省略" "-Wl,--gc-sections" "-pie" "-Wl,-z,relro,-z,now" "-nodefaultlibs" = note: some arguments are omitted. use `--verbose` to show all linker arguments = note: rust-lld: error: unable to find library -lsqlite3 collect2: error: ld returned 1 exit status
使用するSQLiteをコントロールしたい場合は話が変わってきますね。
では、Rusqliteを使っていきましょう。テストコードの雛形はこちら。
src/lib.rs
#[cfg(test)] mod tests { use rusqlite::{Connection, Result, named_params}; use std::fs; use uuid::Uuid; // ここにテストを書く }
まずはインメモリーでデータベースを開いてみます。
#[test] fn connect_in_memory() { let connection = Connection::open_in_memory().unwrap(); let result: i32 = connection .query_one("select 1", (), |row| row.get(0)) .unwrap(); assert_eq!(result, 1); assert_eq!(connection.is_autocommit(), true); }
Struct Connection / pub fn open_in_memory() -> Result
データをファイルに保存する場合。
#[test] fn connect_file_database() { let connection = Connection::open("./first_db.db3").unwrap(); let result: i32 = connection .query_one("select 1", (), |row| row.get(0)) .unwrap(); assert_eq!(result, 1); assert_eq!(connection.is_autocommit(), true); let _ = fs::remove_file("./first_db.db3"); }
Struct Connection / pub fn open<P: AsRef
データベース接続を作る時にはフラグ(OpenFlags)を指定できます。デフォルトだと以下と同じ意味のようです。
Connection::open_with_flags( path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX, )
詳しい意味はこちらへ。
Opening A New Database Connection
インメモリーデータベースだと、接続を跨いでデータを維持できないことを確認。
#[test] fn test_in_memory() { let connection = Connection::open_in_memory().unwrap(); connection .execute("create table t(id integer, primary key(id))", ()) .unwrap(); connection .execute("insert into t(id) values(10)", ()) .unwrap(); let result: i32 = connection .query_one("select id from t order by id limit 1", (), |row| row.get(0)) .unwrap(); assert_eq!(result, 10); let connection = Connection::open_in_memory().unwrap(); match connection.query_one("select id from t order by id limit 1", (), |row| { row.get::<usize, i32>(0) }) { Ok(r) => panic!("unexpected result: {:?}", r), Err(err) => assert_eq!(err.to_string(), "no such table: t"), } }
このコードだとConnectionに割り当てている変数名が同じなので、新しいConnectionを開いた時点で前のConnectionは
Dropされます。
SQLの実行はexecuteやquery_〜を使います。
- Struct Connection / pub fn execute<P: Params>(&self, sql: &str, params: P) -> Result
- Struct Connection / pub fn query_one<T, P, F>(&self, sql: &str, params: P, f: F) -> Result
- Struct Connection / pub fn query_row<T, P, F>(&self, sql: &str, params: P, f: F) -> Result
今回は値を直接入れ込んでいますが、バインドパラメーターも使えます。
ファイルを使った場合は、接続を跨いでもデータが維持されています。
#[test] fn test_file() { let database_path = "./example_db.db3"; let connection = Connection::open(database_path).unwrap(); connection .execute("create table t(id integer, primary key(id))", ()) .unwrap(); connection .execute("insert into t(id) values(10)", ()) .unwrap(); let result: i32 = connection .query_one("select id from t order by id limit 1", (), |row| row.get(0)) .unwrap(); assert_eq!(result, 10); let connection = Connection::open(database_path).unwrap(); let result: i32 = connection .query_one("select id from t order by id limit 1", (), |row| row.get(0)) .unwrap(); assert_eq!(result, 10); let _ = fs::remove_file(database_path); }
テスト内でデータベース接続とクリーンアップを行う関数を作成。
fn with_connection<F>(consumer: F) where F: FnOnce(&mut Connection), { struct CleanupDatabase { path: String, } impl Drop for CleanupDatabase { fn drop(&mut self) { let _ = fs::remove_file(&self.path); } } let path = format!("{}_db.db3", Uuid::new_v4()); let mut connection = Connection::open(&path).unwrap(); let _cleanup = CleanupDatabase { path: path.to_string(), }; consumer(&mut connection); }
複数行の取得。
#[derive(Debug)] struct Person { id: i32, first_name: String, last_name: String, age: i32, } #[test] fn test_map_struct() { with_connection(|connection| { connection .execute( "create table person( id integer, first_name text, last_name text, age integer, primary key(id) )", (), ) .unwrap(); let mut statement = connection .prepare( "insert into person(id, first_name, last_name, age) values(?1, ?2, ?3, ?4)", ) .unwrap(); statement.execute((1, "カツオ", "磯野", 11)).unwrap(); statement.execute((2, "ワカメ", "磯野", 9)).unwrap(); statement.execute((3, "タラオ", "フグ田", 3)).unwrap(); let mut statement = connection .prepare("select id, first_name, last_name, age from person where age > :age order by age desc") .unwrap(); let people: Vec<Person> = statement .query_map(named_params! {":age": 5}, |row| { Ok(Person { id: row.get(0).unwrap(), first_name: row.get(1).unwrap(), last_name: row.get(2).unwrap(), age: row.get(3).unwrap(), }) }) .unwrap() .collect::<Result<Vec<Person>>>() .unwrap(); assert_eq!(people.len(), 2); let katsuo = &people[0]; assert_eq!(katsuo.id, 1); assert_eq!(katsuo.first_name, "カツオ"); assert_eq!(katsuo.last_name, "磯野"); assert_eq!(katsuo.age, 11); let wakame = &people[1]; assert_eq!(wakame.id, 2); assert_eq!(wakame.first_name, "ワカメ"); assert_eq!(wakame.last_name, "磯野"); assert_eq!(wakame.age, 9); }); }
今回はStatementを1度作成し、バインドパラメーターを渡して実行するという方法をとっています。
パラメーターの渡し方は、位置指定と名前指定の2種類があります。
最後はトランザクションの確認。
#[test] fn test_transaction() { with_connection(|connection| { connection .execute( "create table person( id integer, first_name text, last_name text, age integer, primary key(id) )", (), ) .unwrap(); let transaction = connection.transaction().unwrap(); transaction.execute( "insert into person(id, first_name, last_name, age) values(:id, :first_name, :last_name, :age)", named_params! {":id": 1, ":first_name": "カツオ", ":last_name": "磯野", ":age": 11}).unwrap(); transaction.commit().unwrap(); assert_eq!( connection .query_one("select count(*) from person", (), |row| row .get::<usize, i32>(0)) .unwrap(), 1 ); assert_eq!( connection .query_one( "select first_name from person where id = :id", named_params! {":id": 1}, |row| row.get::<usize, String>(0) ) .unwrap(), "カツオ" ); let transaction = connection.transaction().unwrap(); transaction.execute( "insert into person(id, first_name, last_name, age) values(:id, :first_name, :last_name, :age)", named_params! {":id": 2, ":first_name": "ワカメ", ":last_name": "磯野", ":age": 9}).unwrap(); transaction.rollback().unwrap(); assert_eq!( connection .query_one("select count(*) from person", (), |row| row .get::<usize, i32>(0)) .unwrap(), 1 ); assert_eq!( connection .query_one( "select first_name from person where id = :id", named_params! {":id": 2}, |row| row.get::<usize, String>(0) ) .unwrap_err() .to_string(), "Query returned no rows" ); }) }
Transaction in rusqlite - Rust
デフォルトではdropされる時にトランザクションはロールバックされますが、これは変更可能なようです。コミットする時には
明示的に書いた方がよいとは思いますが。
またConnection#executeでバインドパラメーターを使う例にもしています。
こんなところでしょうか。
おわりに
Rusqliteを使ってSQLiteにアクセスしてみました。
まあ、苦労しました…。だいぶRustを忘れているところに引っ張られましたが、いい勉強にはなったのかなと。
SQLiteはいろいろ使いどころがありそうなので、もうちょっと深堀したいところです。