前置き
sqlglotというSQLパーサーをいじっていたらバグを発見しました。
github.com
下記のように全角スペースを含むSQLをパースしようとするとパース出来ないエラーとなりました。(SELECT
の後に全角スペースがあります。)
SELECT * FROM tbl;
しかし、以前試したときは特にエラーが起きないことを確認していたので違和感を覚えました。
verによる違いかなと考えましたが、changelogを見てもそれらしき差分が無かったためコードを深堀してみました。
結論としては、sqlglotのtokenizerは、Python実装のものとRust実装のものがあり、Rust実装のものは全角スペースを変数tokenと認識してしまっていたものによるものでした。
tokenizeの部分はPythonもRustもエラーを起こさずに通過しますが、Parseの部分でRustのtokenizerを通過したtokenはエラーとなるようです。
以前はPython実装のものを使っていたためエラーが出なかったようです。(インストールをGitHubに書かれているやり方ではなくPyPIでやったためこのような状況になったようです。)
Issueにあるようにvenvで環境を切り替えずともSQLGLOTRS_TOKENIZER
環境変数で制御できるようです。
github.com
私がこの件で興味を持ったのは、PythonからRustを呼ぶ仕組みです。
この仕組みを習得すれば、Python言語のメリット(ライブラリが充実していること、簡単に書けること)を全面的に採用しつつ、パフォーマンスが要求される部分のみRust言語で書くという選択肢を得ることができそうです。
maturinの仕組み
調べたところmaturinというツールを使うことでPythonとRustを連携できるようです。
maturinとは、Rustで書かれたコードをPythonの拡張モジュールとしてビルド・配布するためのツールのようです。
こちらの記事にわかりやすい説明が記載されていました。
gihyo.jp
PyO3は、PythonとRustをバインディングするためのツールであり、maturinはそのPyO3を内部で利用し、RustプログラムをビルドしてPythonパッケージ化するツールという位置付けのようです。
また、Polarsやruffといった高速化を特徴とするツール(従来のpandasやblackに対抗するもの)も、PyO3を活用してRustとの連携が図られ、開発されているようです。
maturinの検証
venv環境を作成します。
$ cd project $ python -m venv .venv $ source .venv/bin/activate $ pip install maturin
次にmaturinでプロジェクトの初期化を行います。
$ maturin init $ tree . ├── Cargo.toml ├── README.md ├── pyproject.toml └── src └── lib.rs 1 directory, 4 files
src/lib.rs
は下記のようになっています。
use pyo3::prelude::*; /// Formats the sum of two numbers as string. #[pyfunction] fn sum_as_string(a: usize, b: usize) -> PyResult<String> { Ok((a + b).to_string()) } /// A Python module implemented in Rust. #[pymodule] fn prime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) }
続いてRustプログラムをビルドします。
ビルドにはdevelop
を使い、リリース時には--release
フラグをつけることでデバッグ情報などを除外した最適化された生成物をリリースできるようです。
この操作により、.venv
にバイナリがインストールされるようです。
$ maturin develop ~~ $ ls .venv/lib/python3.13/site-packages/prime/ __init__.py __pycache__ prime.cpython-313-x86_64-linux-gnu.so
ちなみにprime.cpython-313-x86_64-linux-gnu.so
のファイル形式の見方としては、
prime .cpython-313 -x86_64 -linux-gnu .so ────── ─────────── ─────── ────────── ─── モジュール CPythonの CPU OS 共有オブ 名 バージョン アーキ 環境 ジェクト (Python 3.13) テクチャ (Linux) ファイル
prime
: Pythonモジュールとしてimport primeのように使用可能.cpython-313
: Pythonのバージョン-x86_64
: 64ビットのx86アーキテクチャ向けにコンパイルされていることを示す-linux-gnu
: LinuxのGNU Cライブラリ(glibc)を使用する環境向けであることを示す.so
: 共有オブジェクトファイル(Shared Object)の拡張子
プログラムサイズの削減やライブラリ更新の一元化を目的として、プログラムが実行時に動的にライブラリを読み込んで使用する方式を動的リンクと呼びます。
また、この動的リンクされた形式のファイルは共有オブジェクトファイルと呼ばれます。
次のようなmain.py
を作るとRustで記述したモジュールを呼ぶことができました。
import prime if __name__ == "__main__": print(prime.sum_as_string(1, 2))
少し複雑なメソッドを追加するにはこのようにすると出来ました。
use pyo3::prelude::*; /// Formats the sum of two numbers as string. #[pyfunction] fn sum_as_string(a: usize, b: usize) -> PyResult<String> { Ok((a + b).to_string()) } #[pyfunction] fn is_prime(n: u64) -> PyResult<bool> { // 2未満の数は素数ではない if n < 2 { return Ok(false); } // 2は素数 if n == 2 { return Ok(true); } // 偶数は2以外素数ではない if n % 2 == 0 { return Ok(false); } // 3から数の平方根までの奇数で割り切れるかチェック let sqrt = (n as f64).sqrt() as u64; for i in (3..=sqrt).step_by(2) { if n % i == 0 { return Ok(false); } } Ok(true) } /// A Python module implemented in Rust. #[pymodule] fn prime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; m.add_function(wrap_pyfunction!(is_prime, m)?)?; Ok(()) }
import prime if __name__ == "__main__": print(prime.sum_as_string(1, 2)) print(prime.is_prime(7)) # True print(prime.is_prime(8)) # False