2024年良かったもの
2024年良かったもの
💻ガジェット
Airag は今年の前半に物をなくしたり、どこに置いたか忘れてしまったりが頻発することがあったので購入。 一度財布が入ったカバンを置き忘れてしまったときに、場所を特定することに役立った。
Qi2 対応のワイヤレス充電器は、MagSafe でしっかり固定されるのがいい感じ。
♬音楽
新しい音楽も Spotify のレコメンドのおかげで聞かないわけではないが、昔から好きなミュージシャンの新譜はどうしても聞いてしまう。
2023 年末 ~ 2024 年リリースまでの縛りを勝手に設けているけど、やはり今年は THEE MICHELLE GUN ELEPHANT もよく聴いた。
📘本
『なぜ働いていると本が読めなくなるのか』は新書で、タイトルにもなってるテーマ以上に、日本の労働と読書史が面白かった。 これを読んだ派生で、新自由主義に関する本やトクヴィルの本を読むきっかけになって良かった。
『ダキョウソウ』は奇想天外な設定だらけのショートショート系の本。 昨年から好んで聞いてるラジオの聞き手の方の本で、リアリティの上に一つだけ奇々怪々な設定が乗ることで不思議な世界観を体感できる。この書評が好き。
📻ラジオ
俺のメモ帳は、ロバート秋山が YouTube でやってる世界観をラジオ番組の中で表現している感じ。看板コーナーの生活音のコーナーが好き。
聞けば無罪は、こたけ正義感の耳心地の良い声と軽快なトークが聞いていて丁度いい。
ブタピエロは地上波進出しても Podcast 時代と変わらず面白い。
🍺ビール
- DIPA V15 / Cloudwater Brew Co.
- Think It Over, Think It Under / The Answer
- TDHSSR (Super Sunrise) / West Coast Brewing
- Amorphous Haze / Choshi Beer
Hazy が好きなのでその系統が多め。
DIPA V15 は dunk と呼ばれる味わいを美味しいと感じた。
Think It Over, Think It Under は変な苦みを感じず、フルーティーさもかなりして美味しかった。
TDHSSR (Super Sunrise) もトロピカルさが強調された、かなり好みの味だった。
Amorphous Haze は千葉県銚子市のクラフトビールメーカーのもので自分好みのフルーティーさだった。値段が比較的高くないのも良い。
その他
Switch オンラインの 64 パックがノスタルジーを誘う。
普段全く飲まないジンのソーダ割りをたまたま飲んだときに、その華やかな香りに衝撃を覚えた。八王子のクラフトジンというのも何だか気取らない感じでいい。
30 代になって疲れが取れない実感を感じてきた。就寝前に飲むと、起床後が比較的楽になった気分がする。
click を使ったエラーケースのテスト方法
背景
コマンドラインインターフェイスの click のテスト方法についてのメモ
実装
コード
以下のコードをテストしたい場合を考える。
# src/main.py import click @click.group() def cli(): pass @cli.command() @click.option("--name", type=str, required=True, help="The person to greet.") def hello(name): if name == "Bob": raise ValueError("Sorry, Bob is not allowed.") click.echo(f"Hello {name}!") @cli.command() @click.option("--name", type=str, required=True, help="The person to greet.") def goodbye(name): click.echo(f"Goodbye {name}!")
# src/__main__.py from src.main import cli if __name__ == "__main__": cli()
通常の実行例は以下のようになる想定。
python -m src hello --name Alice
テスト方法
click.testing.CliRunner
click のテストでは click.testing.CliRunner を使用する。
Testing Click Applications — Click Documentation (8.1.x)
上記の例では Bob という引数が来たらエラーになるので、そのテストをしたいとする。
Pytest でのエラーケースのテストは pytest.raises を使うので、同様のコードを書くと以下のようになる。
import pytest from click.testing import CliRunner def test_cli(name): from src.main import cli runner = CliRunner() with pytest.raises(ValueError): runner.invoke(cli, ["hello", "--name", "Bob"])
しかし、このテストを実行するとエラーが出る。
CliRunner.invoke 自体の実行はエラーが生じないためと思われる。
> with pytest.raises(ValueError): E Failed: DID NOT RAISE <class 'ValueError'> tests/test_main.py:9: Failed
click.testing.Result
click のテストでエラーが生じる場合のテストは CliRunner.invoke の返り値を利用する
import pytest from click.testing import CliRunner def test_cli(): from src.main import cli runner = CliRunner() result = runner.invoke(cli, ["hello", "--name", "Bob"]) assert result.exit_code != 0 assert isinstance(result.exception, ValueError) assert str(result.exception) == "Sorry, Bob is not allowed."
以上を踏まえて、正常系と異常系のテストケースをまとめると以下のようになる。
import pytest from click.testing import CliRunner @pytest.mark.parametrize( "name, expected_error", [ ("Alice", None), ("Bob", ValueError("Sorry, Bob is not allowed.")), ], ) def test_cli(name, expected_error): from src.main import cli runner = CliRunner() result = runner.invoke(cli, ["hello", "--name", name]) if expected_error: assert result.exit_code != 0 assert isinstance(result.exception, ValueError) assert str(result.exception) == str(expected_error) else: assert result.exit_code == 0 assert result.output == f"Hello {name}!\n"
まとめ
- click のテストでは
click.testing.CliRunnerを使用する - 異常系のテストでは
CliRunner.invokeの返り値のResult.exit_codeとResult.exceptionを利用する
2023年良かったもの
2023年良かったもの
💻ガジェット
- なし
♬音楽
📘本
📻ラジオ
その他
- スーパーマリオRPG
- 子供の頃のノスタルジーを刺激するゲーム。あの頃難しくてよく分からなかった箇所も分かるようになる一種の寂しさ。
- 無印良品 パルプボードボックス
- 5段のものを複数並べているが高さがあるので空間に対する収納力が増えて良い。
- リンテックコマース 結露給水シート
- 冬の間、窓の結露を毎日拭かなくても良くなった。
cargo build 時に error: failed to run custom build command for `openssl-sys v0.9.97` が出たときの対処法
エラーメッセージ
cargo-lambda でビルドしようとしたときに以下のようなエラーが出た
$ cargo lambda build --release --arm64 --bin lambda Compiling openssl-sys v0.9.97 error: failed to run custom build command for `openssl-sys v0.9.97` Caused by: process didn't exit successfully: `$PWD/target/release/build/openssl-sys-84db175de3593bc2/build-script-main` (exit status: 101) --- stdout cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR unset cargo:rerun-if-env-changed=OPENSSL_LIB_DIR OPENSSL_LIB_DIR unset cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR unset cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR OPENSSL_INCLUDE_DIR unset cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_DIR unset cargo:rerun-if-env-changed=OPENSSL_DIR OPENSSL_DIR unset cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_ALLOW_CROSS cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS cargo:rerun-if-env-changed=PKG_CONFIG_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_SYSROOT_DIR cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR run pkg_config fail: pkg-config has not been configured to support cross-compilation. Install a sysroot for the target platform and configure it via PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a cross-compiling wrapper for pkg-config and set it via PKG_CONFIG environment variable. --- stderr thread 'main' panicked at ' Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. Make sure you also have the development packages of openssl installed. For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora. If you're in a situation where you think the directory *should* be found automatically, please open a bug at https://github.com/sfackler/rust-openssl and include information about your system as well as this message. $HOST = aarch64-apple-darwin $TARGET = aarch64-unknown-linux-gnu openssl-sys = 0.9.97 ', $HOME/.cargo/registry/src/index.crates.io-6f17d22bba15001f/openssl-sys-0.9.97/build/find_normal.rs:190:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
対応策
エラーメッセージ中の以下の箇所に注目して対応してみる
Install a sysroot for the target platform and configure it via PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a cross-compiling wrapper for pkg-config and set it via PKG_CONFIG environment variable.
pkg-config のクロスコンパイルラッパーをインストールして PKG_CONFIG という環境変数にセットする
brew install pkg-config-wrapper
pkg-config-wrapper のパスを確認する
$ which pkg-config-wrapper /opt/homebrew/bin/pkg-config-wrapper
PKG_CONFIG という環境変数に確認したパスを設定する
export PKG_CONFIG=/opt/homebrew/bin/pkg-config-wrapper
ビルドが通った
$ cargo lambda build --release --arm64 --bin lambda Compiling openssl-sys v0.9.97 Compiling openssl v0.10.61
Cargo Lambda で AWS Lambda 用の Rust バイナリファイルをビルドする
背景
個人的に家計管理のために Slack に投稿すると IFTTT 経由でスプレッドシートに転記するような AWS Lambda を作って使っていた。

Go 1.x が Deprecation になるのに合わせて Go で書いていたアプリケーションを Rust に移植してみた。 そのときの手順やハマったポイントについて書き残しておく。
ちなみに Python 版の話は以前ブログで書いた。
方法
Rust で書かれた AWS Lambda 関数を便利にビルド・デプロイできるパッケージが AWS 公式でリリースされている。 基本的に手順は README を読みながら進めれば問題なくビルド・デプロイできる。
Cargo Lambda 自体のドキュメントは以下にまとめられている。
手順
1. Cargo Lambda をインストールする
brew tap cargo-lambda/cargo-lambda brew install cargo-lambda
2. Lambda 関数を実装する
Cargo Lambda のサブコマンド new を利用すると雛形が作られるので、それをもとに修正を行う。
cargo lambda new $LAMBDA_PACKAGE_NAME
以下のインタラクティブを経て生成される。
? Is this function an HTTP function? (y/N) [type `yes` if the Lambda function is triggered by an API Gateway, Amazon Load Balancer(ALB), or a Lambda URL] ? AWS Event type that this function receives activemq::ActiveMqEvent autoscaling::AutoScalingEvent chime_bot::ChimeBotEvent cloudwatch_events::CloudWatchEvent cloudwatch_logs::CloudwatchLogsEvent cloudwatch_logs::CloudwatchLogsLogEvent v codebuild::CodeBuildEvent [↑↓ to move, tab to auto-complete, enter to submit. Leave it blank if you don't want to use any event from the aws_lambda_events crate]
1つ目の質問を No、2つ目の質問を空白で答えると以下のようなコードが生成される
use lambda_runtime::{run, service_fn, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; /// This is a made-up example. Requests come into the runtime as unicode /// strings in json format, which can map to any structure that implements `serde::Deserialize` /// The runtime pays no attention to the contents of the request payload. #[derive(Deserialize)] struct Request { command: String, } /// This is a made-up example of what a response structure may look like. /// There is no restriction on what it can be. The runtime requires responses /// to be serialized into json. The runtime pays no attention /// to the contents of the response payload. #[derive(Serialize)] struct Response { req_id: String, msg: String, } /// This is the main body for the function. /// Write your code inside it. /// There are some code example in the following URLs: /// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples /// - https://github.com/aws-samples/serverless-rust-demo/ async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> { // Extract some useful info from the request let command = event.payload.command; // Prepare the response let resp = Response { req_id: event.context.request_id, msg: format!("Command {}.", command), }; // Return `Response` (it will be serialized to JSON automatically by the runtime) Ok(resp) } #[tokio::main] async fn main() -> Result<(), Error> { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) // disable printing the name of the module in every log line. .with_target(false) // disabling time is handy because CloudWatch will add the ingestion time. .without_time() .init(); run(service_fn(function_handler)).await }
生成された function_handler 関数が実行のメインなのでここに実装を行う。
別パッケージで処理を行うハンドラを書いておいて、それを呼び出すだけにした方が実装が簡潔になると思う。
注意点としては、 aws lambda invoke で --payload の key になる値に Request 構造体のフィールド名を一致させる必要がある。
3. Lambda 関数をビルドする
Cargo Lambda のサブコマンド build でビルドできる。
--release オプションでリリースモードでビルドされるところや、--arm64 オプションで arm64 アーキでビルドされる(--target aarch64-unknown-linux-gnu の省略形)ところは通常の cargo と同じ。
ちなみに --output-format でバイナリファイルが Zip された状態で生成することもできる。後述の cargo deploy を使わずにデプロイする場合には指定すると良いかもしれない。
cargo lambda build --release --arm64 --bin $LAMBDA_PACKAGE_NAME
4. Lambda 関数をデプロイする
cargo lambda のサブコマンド deploy でデプロイできる。
環境変数も AWS CLI のように一つ一つ指定しなくても --env-file オプションで KEY=VALUE フォーマットで記述された .env ファイルのパスを渡すだけで設定できる。
Lambda の実行ロールは指定しなければ、デフォルトのロールが作成される。また細かい設定(ハンドラ名、Lambda パッケージのパス、アーキテクチャ等)の指定も不要。
cargo lambda deploy $LAMBDA_FUNCTION_NAME --env-file .env
5. Lambda 関数を実行する(テストする)
実行も cargo lambda のサブコマンド invoke で実行できる。
--remote オプションですでに AWS Lambda にデプロイされた関数を実行できる。指定しない場合は(デフォルトでは)ローカルのエミュレータに対してリクエストを送ってしまう*1ので注意する必要がある。
また payload が不要な場合でも version 1.0.0 時点では何らかのデータフラグ(--data-file, --data-ascii, --data-example のいずれか)の指定が必要になっている。
cargo lambda invoke $LAMBDA_FUNCTION_NAME --remote --output-format json --data-ascii "{}"
ハマったポイント
いくつか実装中にハマったポイントがあったので列挙する。
1. OpenSSL 関連のエラー
OpenSSL 関連のエラーメッセージ例
error: failed to run custom build command for `openssl-sys v0.9.97` Caused by: process didn't exit successfully: `$PWD/target/release/build/openssl-sys-b2444d8dd69cea00/build-script-main` (exit status: 101) --- stdout cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR unset cargo:rerun-if-env-changed=OPENSSL_LIB_DIR OPENSSL_LIB_DIR unset cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR unset cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR OPENSSL_INCLUDE_DIR unset cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_DIR unset cargo:rerun-if-env-changed=OPENSSL_DIR OPENSSL_DIR unset cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_ALLOW_CROSS cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS cargo:rerun-if-env-changed=PKG_CONFIG_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_SYSROOT_DIR cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR run pkg_config fail: pkg-config has not been configured to support cross-compilation. Install a sysroot for the target platform and configure it via PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a cross-compiling wrapper for pkg-config and set it via PKG_CONFIG environment variable. --- stderr thread 'main' panicked at ' Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. Make sure you also have the development packages of openssl installed. For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora. If you're in a situation where you think the directory *should* be found automatically, please open a bug at https://github.com/sfackler/rust-openssl and include information about your system as well as this message. $HOST = aarch64-apple-darwin $TARGET = aarch64-unknown-linux-gnu openssl-sys = 0.9.97 ', $HOME/.cargo/registry/src/index.crates.io-6f17d22bba15001f/openssl-sys-0.9.97/build/find_normal.rs:190:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
この問題は Cargo Lambda のドキュメントにも実は書かれていた。
自分のケースだと reqwest クレートが TLS バックエンドとして OpenSSL を使用しているため rustls feature (あるいは native-tls-vendored feature)を有効にして、TLS バックエンドをアプリケーションに含めるようにする必要があるらしい。
Cross Compiling with Cargo Lambda | Cargo Lambda
というのも AWS Lambda は Linux サンドボックス環境で関数が実行され、サンドボックスはOSが動作するために必要なライブラリのみ含んでいるため、 *-sys ライブラリ群はバイナリに完全にリンクされているかネイティブの依存関係を他の方法で提供しない限り動作しない可能性があるためらしい。
そのため自分は Cargo.toml の reqwest クレートの記述を修正することでビルドを通すことができた。
-reqwest = { version = "0.11", features = ["blocking", "json"] } +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
2. Request 構造体のフィールドの DeserializeError
cargo lambda invoke で payload の key になる値に Request 構造体のフィールド名を一致させる必要がある。
payload の値を受けて serde::Deserialize するため DeserializeError が発生する。以下は Request 構造体に command というフィールドがあるにもかかわらず空の payload を渡した場合に起こるエラー例。
$ cargo lambda invoke $LAMBDA_FUNCTION_NAME --remote --data-ascii "{}"
Error: lambda_runtime::deserializer::DeserializeError
× failed to deserialize the incoming data into the function's payload type: missing field `command` at line 1 column 2
もし payload が不要な場合には Request 構造体はフィールドを持たない空の構造体にする。
3. Invoke サブコマンドで Connection refused (os error 61) エラー
前述の通り、すでに AWS Lambda にデプロイされた関数を実行したい場合には、--remote オプションの指定が必須。
指定しない場合はローカルのエミュレータに対してリクエストを送ってしまい、エミュレータを立ち上げていない場合 Connection refused (os error 61) エラーが発生してしまう。
$ cargo lambda invoke $LAMBDA_FUNCTION_NAME --output-format json --data-ascii "{}"
Error: × error sending request to the runtime emulator
├─▶ error sending request for url (http://[::1]:9000/2015-03-31/functions/$LAMBDA_FUNCTION_NAME/invocations): error trying to connect: tcp connect error: Connection refused (os error 61)
├─▶ error trying to connect: tcp connect error: Connection refused (os error 61)
├─▶ tcp connect error: Connection refused (os error 61)
╰─▶ Connection refused (os error 61)
結果
無事に Rust 製の Lambda 関数をビルド・デプロイできた。
実行の頻度も時間も重くないので、特段気になるところではないが、一応 Billed Duration も Max Memory Used も大きく減った。
Go の Lambda 関数は 600ms, 35MB 程度だったが、Rust だと 400ms, 18MB 程度になっていた*2。
以上。
作業リポジトリ: github.com
*1:ソースコードでの分岐箇所: https://github.com/cargo-lambda/cargo-lambda/blob/a342f0b8a7c34b17cee2f4fed3cc7d3ddbd810ab/crates/cargo-lambda-invoke/src/lib.rs#L156-L160
*2:Go は norpc オプションを有効にしてないなど最適化の余地が残ってるかもしれない
Rusqlite クレートを使って Rust で SQLite を利用する
概要
Rust で SQLite を使うためのラッパークレートである Rusqlite を使ってみる。
Python で SQLite バイナリを作成し、それを Rust で Rusqlite を使って読み込みクエリを実行する流れのサンプルコードを書いてみた。
Python で作成した SQLite バイナリを Rust で読み込んでクエリを実行する
ディレクトリ構成
以下のようなディレクトリ構成になってる想定でコードを書いていく。
.
├── python
│ └── python3.10
│ ├── data
│ │ └── users.sqlite
│ ├── poetry.lock
│ ├── pyproject.toml
│ ├── README.md
│ ├── src
│ │ ├── __init__.py
│ │ └── __main__.py
│ └── tasks.py
└── rust
├── Cargo.lock
├── Cargo.toml
├── Makefile.toml
├── README.md
└── src
└── main.rs
Python で SQLite のバイナリを作成する
使用するサードパーティライブラリはないので pyproject.toml は不要かもしれない。
- pyproject.toml
[tool.poetry]
name = "python3-10"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
packages = [{include = "src"}]
[tool.poetry.dependencies]
python = "~3.10"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
以下のように、テーブル作成・値の挿入を行ったデータベースを保存する。
テスト代わりに Python 側でクエリを実行できるかを最後に確認している。
- main.py
import sqlite3 CREATE_QUERY = """ CREATE TABLE users ( id INTEGER, age INTEGER NOT NULL, name TEXT NOT NULL, PRIMARY KEY (id) ); """ INSERT_QUERY = """ INSERT INTO users VALUES (1, 20, 'Alice'), (2, 30, 'Bob'), (3, 40, 'Carol'); """ SELECT_QUERY = """ SELECT * FROM users; """ def main(): path = "data/users.sqlite" with open(path, mode="w") as f: conn = sqlite3.connect(f.name) cursor = conn.cursor() cursor.execute(CREATE_QUERY) cursor.execute(INSERT_QUERY) conn.commit() conn.close() with open(path, mode="r") as f: conn = sqlite3.connect(f.name) cursor = conn.cursor() cursor.execute(SELECT_QUERY) print(cursor.fetchall()) if __name__ == "__main__": main()
- 出力
[(1, 20, 'Alice'), (2, 30, 'Bob'), (3, 40, 'Carol')]
参考: Python: テストで SQLite3 のインメモリデータベースを使うときの問題点と解決策 - CUBE SUGAR CONTAINER
Rust で SQLite バイナリを読み込んでクエリを実行する
Cargo.toml に rusqlite を追加する。
features には bundled と backup を指定する。
前者はユーザーシステムの SQLite への自動リンクを可能にし、後者は SQLite バイナリからデータベースを復元する際に必要となる。
- Cargo.toml
[package]
name = "rust"
version = "0.1.0"
edition = "2021"
[dependencies]
rusqlite = { version = "0.29.0", features = ["bundled", "backup"] }
Connection::open_in_memory() でインメモリな SQLite データベースを構築し、それに対して restore() することで復元する。
その後はクエリを実行し、Rust で定義した構造体にクエリの実行結果をマップする。
出力内容が Python のものと一致していることが確認できる。
- main.rs
use rusqlite::{Connection, DatabaseName}; #[derive(Debug)] #[allow(dead_code)] struct User { id: i32, age: i32, name: String, } fn main() { let path = "python/python3.10/data/users.sqlite"; let path = format!("{}/../{}", env!("CARGO_MANIFEST_DIR"), path); let mut conn = Connection::open_in_memory().unwrap(); conn.restore(DatabaseName::Main, &path, Some(|_| {})).unwrap(); let mut stmt = conn.prepare("SELECT id, age, name FROM users").unwrap(); let user_iter = stmt.query_map([], |row| { Ok(User { id: row.get(0).unwrap(), age: row.get(1).unwrap(), name: row.get(2).unwrap(), }) }).unwrap(); for user in user_iter { println!("Found user {:?}", user.unwrap()); } }
- 出力
Found user User { id: 1, age: 20, name: "Alice" }
Found user User { id: 2, age: 30, name: "Bob" }
Found user User { id: 3, age: 40, name: "Carol" }
参考: GitHub - rusqlite/rusqlite: Ergonomic bindings to SQLite for Rust
まとめ
Rustqlite を使えば Rust でも SQLite を気軽に利用できそう。
Rust の Result と Option のメソッドまとめ
※ 項目の末尾の * は Result あるいは Option にしかないメソッドであることを表す。
概要
Rust には様々な標準ライブラリが用意されています。
その中には Result 型や Option 型といった便利な型があります。
これらにはいくつかのメソッドが用意されており、エラーや None の値を簡潔かつ適切に処理することができます。
この記事では Result 型と Option 型のいくつかのメソッドについてまとめて、列挙していきます。
std::Result
Rust には Result 型と言われるものがあり、これは関数が成功するか失敗するかの結果を表現する列挙型です。
一般的には、成功時には Ok 値を、失敗時には Err 値を返します。
Result 型は、 Rust のエラーハンドリングにおいて非常に重要な役割を担っています。
pub enum Result<T, E> { Ok(T), Err(E), }
map
Okの場合には関数を適用してErrの場合にはそのままにしてResultを返す
pub fn map<U, F>(self, op: F) -> Result<U, E> where F: FnOnce(T) -> U,
Example
let line = "1\n2\n3\n4\n"; for num in line.lines() { match num.parse::<i32>().map(|i| i * 2) { Ok(n) => println!("{n}"), Err(..) => {} } }
map_err*
Okの場合にはそのままにしてErrの場合には関数を適用してResultを返す
pub fn map_err<F, O>(self, op: O) -> Result<T, F> where O: FnOnce(E) -> F,
Example
fn stringify(x: u32) -> String { format!("error code: {x}") } let x: Result<u32, u32> = Ok(2); assert_eq!(x.map_err(stringify), Ok(2)); let x: Result<u32, u32> = Err(13); assert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));
map_or
Okの場合には関数を適用してErrの場合にはデフォルト値を返す
pub fn map_or<U, F>(self, default: U, f: F) -> U where F: FnOnce(T) -> U,
Example
let x: Result<_, &str> = Ok("foo"); assert_eq!(x.map_or(42, |v| v.len()), 3); let x: Result<&str, _> = Err("bar"); assert_eq!(x.map_or(42, |v| v.len()), 42);
map_or_else
Okの場合には関数を適用してErrの場合にはフォールバック関数を適用する
pub fn map_or_else<U, D, F>(self, default: D, f: F) -> U where D: FnOnce(E) -> U, F: FnOnce(T) -> U,
Example
let k = 21; let x : Result<_, &str> = Ok("foo"); assert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 3); let x : Result<&str, _> = Err("bar"); assert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 42);
unwrap
Okの場合には値をそのまま返してErrの場合にはパニックを起こす
pub fn unwrap(self) -> T where E: Debug,
Example
let x: Result<u32, &str> = Ok(2); assert_eq!(x.unwrap(), 2); // This example panics let x: Result<u32, &str> = Err("emergency failure"); x.unwrap(); // panics with `emergency failure`
unwrap_err*
Okの場合にはパニックを起こしてErrの場合には値をそのまま返す
pub fn unwrap_err(self) -> E where T: Debug,
Example
// This example panics let x: Result<u32, &str> = Ok(2); x.unwrap_err(); // panics with `2` let x: Result<u32, &str> = Err("emergency failure"); assert_eq!(x.unwrap_err(), "emergency failure");
unwrap_or
Okの場合には値をそのまま返してErrの場合には引数の値をデフォルト値として返す
pub fn unwrap_or(self, default: T) -> T
Example
let default = 2; let x: Result<u32, &str> = Ok(9); assert_eq!(x.unwrap_or(default), 9); let x: Result<u32, &str> = Err("error"); assert_eq!(x.unwrap_or(default), default);
unwrap_or_else
Okの場合には値をそのまま返してErrの場合には引数で受けたクロージャの返り値を返す
pub fn unwrap_or_else<F>(self, op: F) -> T where F: FnOnce(E) -> T,
Example
fn count(x: &str) -> usize { x.len() } assert_eq!(Ok(2).unwrap_or_else(count), 2); assert_eq!(Err("foo").unwrap_or_else(count), 3);
unwrap_or_default
Okの場合には値をそのまま返してErrの場合にはOkの型のデフォルト値を返す
pub fn unwrap_or_default(self) -> T where T: Default,
Example
let good_year_from_input = "1909"; let bad_year_from_input = "190blarg"; let good_year = good_year_from_input.parse().unwrap_or_default(); let bad_year = bad_year_from_input.parse().unwrap_or_default(); assert_eq!(1909, good_year); assert_eq!(0, bad_year);
and_then
Okの場合には関数を呼び出してErrの場合には値をそのままにしてResultを返すOk型が返ってくるmapと異なり、関数の返り値もResultになっている
pub fn and_then<U, F>(self, op: F) -> Result<U, E> where F: FnOnce(T) -> Result<U, E>,
Example 1
fn sq_then_to_string(x: u32) -> Result<String, &'static str> { x.checked_mul(x).map(|sq| sq.to_string()).ok_or("overflowed") } assert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4.to_string())); assert_eq!(Ok(1_000_000).and_then(sq_then_to_string), Err("overflowed")); assert_eq!(Err("not a number").and_then(sq_then_to_string), Err("not a number"));
Example2: Err を返すかもしれないメソッドチェーンの操作内で用いられる
use std::{io::ErrorKind, path::Path}; // Note: on Windows "/" maps to "C:\" let root_modified_time = Path::new("/").metadata().and_then(|md| md.modified()); assert!(root_modified_time.is_ok()); let should_fail = Path::new("/bad/path").metadata().and_then(|md| md.modified()); assert!(should_fail.is_err()); assert_eq!(should_fail.unwrap_err().kind(), ErrorKind::NotFound);
or_else
Okの場合には値をそのままにしてErrの場合には関数を呼び出してResultを返すErr型が返ってくるmap_errと異なり、関数の返り値もResultになっている
pub fn or_else<F, O>(self, op: O) -> Result<T, F> where O: FnOnce(E) -> Result<T, F>,
Example
fn sq(x: u32) -> Result<u32, u32> { Ok(x * x) } fn err(x: u32) -> Result<u32, u32> { Err(x) } assert_eq!(Ok(2).or_else(sq).or_else(sq), Ok(2)); assert_eq!(Ok(2).or_else(err).or_else(sq), Ok(2)); assert_eq!(Err(3).or_else(sq).or_else(err), Ok(9)); assert_eq!(Err(3).or_else(err).or_else(err), Err(3));
ok
Result<T, E>をOption<T>に変換するErrは潰れてNoneになる
pub fn ok(self) -> Option<T>
Example
let x: Result<u32, &str> = Ok(2); assert_eq!(x.ok(), Some(2)); let x: Result<u32, &str> = Err("Nothing here"); assert_eq!(x.ok(), None);
err
Result<T, E>をOption<E>に変換するOkは潰れてNoneになる
pub fn err(self) -> Option<E>
Example
let x: Result<u32, &str> = Ok(2); assert_eq!(x.err(), None); let x: Result<u32, &str> = Err("Nothing here"); assert_eq!(x.err(), Some("Nothing here"));
std::Option
また、 Rust には Option 型と言われるものがあり、これは値が存在するかどうかを表現するための列挙型です。
Some 値を持つ場合は Some(v) という値を、値が存在しない場合は None という値を持ちます。
Option 型は、Rust においてエラーハンドリングに使われるだけでなく、 Rust のパターンマッチングにおいても頻繁に使われます。
pub enum Option<T> { None, Some(T), }
map
Someの場合には関数を適用してNoneの場合にはそのままにしてOptionを返す
pub fn map<U, F>(self, f: F) -> Option<U> where F: FnOnce(T) -> U,
Example
/* Converts an Option<String> into an Option<usize>, consuming the original: */ let maybe_some_string = Some(String::from("Hello, World!")); // `Option::map` takes self *by value*, consuming `maybe_some_string` let maybe_some_len = maybe_some_string.map(|s| s.len()); assert_eq!(maybe_some_len, Some(13));
map_or
Someの場合には関数を適用してNoneの場合にはデフォルト値を返す
pub fn map_or<U, F>(self, default: U, f: F) -> U where F: FnOnce(T) -> U,
Example
let x = Some("foo"); assert_eq!(x.map_or(42, |v| v.len()), 3); let x: Option<&str> = None; assert_eq!(x.map_or(42, |v| v.len()), 42);
map_or_else
Someの場合には関数を適用してNoneの場合には別のデフォルト関数を適用する
pub fn map_or_else<U, D, F>(self, default: D, f: F) -> U where D: FnOnce() -> U, F: FnOnce(T) -> U,
Example
let k = 21; let x = Some("foo"); assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 3); let x: Option<&str> = None; assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 42);
unwrap
Someの場合には値をそのまま返してNoneの場合にはパニックを起こす
pub fn unwrap(self) -> T
Example
let x = Some("air"); assert_eq!(x.unwrap(), "air"); // This example panics let x: Option<&str> = None; assert_eq!(x.unwrap(), "air"); // fails
unwrap_or
Someの場合には値をそのまま返してNoneの場合には引数の値をデフォルト値として返す
pub fn unwrap_or(self, default: T) -> T
Example
assert_eq!(Some("car").unwrap_or("bike"), "car"); assert_eq!(None.unwrap_or("bike"), "bike");
unwrap_or_else
Someの場合には値をそのまま返してNoneの場合には引数で受けたクロージャの返り値を返す
pub fn unwrap_or_else<F>(self, f: F) -> T where F: FnOnce() -> T,
Example
let k = 10; assert_eq!(Some(4).unwrap_or_else(|| 2 * k), 4); assert_eq!(None.unwrap_or_else(|| 2 * k), 20);
unwrap_or_default
Someの場合には値をそのまま返してNoneの場合にはSomeの型のデフォルト値を返す
pub fn unwrap_or_default(self) -> T where T: Default,
Example
let good_year_from_input = "1909"; let bad_year_from_input = "190blarg"; let good_year = good_year_from_input.parse().ok().unwrap_or_default(); let bad_year = bad_year_from_input.parse().ok().unwrap_or_default(); assert_eq!(1909, good_year); assert_eq!(0, bad_year);
and_then
Someの場合には関数を呼び出してNoneの場合には値をそのままにしてOptionを返すSome型が返ってくるmapと異なり、関数の返り値もOptionになっている
pub fn and_then<U, F>(self, f: F) -> Option<U> where F: FnOnce(T) -> Option<U>,
Example1
fn sq_then_to_string(x: u32) -> Option<String> { x.checked_mul(x).map(|sq| sq.to_string()) } assert_eq!(Some(2).and_then(sq_then_to_string), Some(4.to_string())); assert_eq!(Some(1_000_000).and_then(sq_then_to_string), None); // overflowed! assert_eq!(None.and_then(sq_then_to_string), None);
Example2: Err を返すかもしれないメソッドチェーンの操作内で用いられる
let arr_2d = [["A0", "A1"], ["B0", "B1"]]; let item_0_1 = arr_2d.get(0).and_then(|row| row.get(1)); assert_eq!(item_0_1, Some(&"A1")); let item_2_0 = arr_2d.get(2).and_then(|row| row.get(0)); assert_eq!(item_2_0, None);
or_else
Someの場合には値をそのままにしてNoneの場合には関数を呼び出してOptionを返す
pub fn or_else<F>(self, f: F) -> Option<T> where F: FnOnce() -> Option<T>,
Example
fn nobody() -> Option<&'static str> { None } fn vikings() -> Option<&'static str> { Some("vikings") } assert_eq!(Some("barbarians").or_else(vikings), Some("barbarians")); assert_eq!(None.or_else(vikings), Some("vikings")); assert_eq!(None.or_else(nobody), None);
ok_or*
Some(v)をOk(v)に、NoneをErr(err)にマッピングしてOption<T>をResult<T, E>に変換する
pub fn ok_or<E>(self, err: E) -> Result<T, E>
Example
let x = Some("foo"); assert_eq!(x.ok_or(0), Ok("foo")); let x: Option<&str> = None; assert_eq!(x.ok_or(0), Err(0));
ok_or_else*
Some(v)をOk(v)に、NoneをErr(err())にマッピングしてOption<T>をResult<T, E>に変換する
pub fn ok_or_else<E, F>(self, err: F) -> Result<T, E> where F: FnOnce() -> E,
Example
let x = Some("foo"); assert_eq!(x.ok_or_else(|| 0), Ok("foo")); let x: Option<&str> = None; assert_eq!(x.ok_or_else(|| 0), Err(0));
Tips
Result→Optionはmap_or_elseで None と Some を返すようにして変換できるOption→Resultはok_orあるいはok_or_elseで変換できる