Introduction
以前SQLiteのextensionを使う記事を書いたのですが、
今回はextensionを作るほうです。
sqlite-loadable-rsを使えばRustでSQLite用extensionを作成できるので、
やってみましょう。
Environment
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 13.5.2
- Linux VM : ubuntu23(OrbStack使用)
- Rust : 1.72.1
Setup
sqlite-loadable-rsクレートはRustを使用してSQLite拡張を構築するためのライブラリです。
詳細についてはここに紹介記事がありますのでご確認ください。
sqlite-loadable-rsのリポジトリにはそのまま動くサンプルがあるので、
Githubからcloneして動かしてみましょう。
Linux VM環境を作成
私の環境(M1 mac)だと動かなかったので、Linux VM環境を用意します。
OrbStack使ったら簡単に動いたのでそれを使います。
OrbStackを起動したらMachinesからUbuntu23を選択して起動します。
起動したら↓のようにsshでログインできます。
とても楽です。
% ssh <作成したVMの名前>@orb
Ubuntuで必要なライブラリなどのインストールをします。
% sudo apt update
% sudo apt-get install git
% sudo apt-get install -y sqlite3
% sudo apt-get install -y gcc
% sudo apt-get install libssl-dev
私の環境ではビルド時にssl関連のエラーがでたので、
そうなった場合は↓のコマンドでパスをしらべて環境変数に設定。
% dpkg -L libssl-dev | grep lib
% dpkg -L libssl-dev | grep include
#自分の環境では↓を設定
% export OPENSSL_INCLUDE_DIR=/usr/include/openssl/
% export OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu/
そしてRustとNodeのインストールをしておきます。
# Rust
% curl https://sh.rustup.rs -sSf | sh
% export PATH=$PATH:$HOME/.cargo/bin
# nodeとnpm
% sudo apt install nodejs npm
Try sqlite-loadable-rs
では次に、sqlite-loadable-rsにあるサンプルの拡張ライブラリを動かしてみましょう。
sqlite-loadable-rsをcloneしてhello extentionをうごかしてみます。
% git clone https://github.com/asg017/sqlite-loadable-rs.git
% cd sqlite-loadable-rs
Rustで記述された、hello extentionをビルドします。
% cargo build --example hello --release
sqliteを起動して、↑でビルドしたモジュールをloadします。
hello関数が使えるようになってますね。
% sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
・・・
sqlite> .load target/release/examples/libhello
sqlite> select hello("world!");
hello, world!
sqlite> .quit
Create SQlite Extension
次に、Sqlite Extensionを作ってみます。
fooという関数を定義し、その中ではhttpbin.orgにアクセスして
自身のIPをとってくるという(特に意味のない)処理をしてみます。
Cargoでプロジェクトを作成して依存ライブラリをaddします。
% cargo new sqlite-plugin && cd sqlite-plugin
% cargo add sqlite-loadable
% cargo add reqwest
Cargo.tomlは↓のような感じです。
[dependencies]
reqwest = { version = "0.11.20", features = ["blocking", "json"] }
sqlite-loadable = "0.0.5"
[[example]]
name = "foo"
crate-type = ["cdylib"]
sqlite-plugin/examplesディレクトリを作成し、
foo.rsファイル(extension本体)を作成します。
use sqlite_loadable::prelude::*;
use sqlite_loadable::{api, define_scalar_function, Result};
use std::collections::HashMap;
pub fn foo(context: *mut sqlite3_context, values: &[*mut sqlite3_value]) -> Result<()> {
let key = api::value_text(values.get(0).expect("1st argument as key"))?;
let name = api::value_text(values.get(1).expect("2nd argument as name"))?;
//外部サイトに接続してデータ取得
let resp = reqwest::blocking::get("https://httpbin.org/ip")
.unwrap().json::<HashMap<String, String>>().unwrap();
api::result_text(context, format!("1st:{}, 2nd:{}, ip:{:?}", key,name,resp))?;
Ok(())
}
#[sqlite_entrypoint]
pub fn sqlite3_foo_init(db: *mut sqlite3) -> Result<()> {
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC;
define_scalar_function(db, "foo", 2, foo, flags)?;
Ok(())
}
foo関数はそのまま"foo"という名前のscalar関数として登録されます。
この関数では引数を2つ受け取り、内部では自身のIPアドレスを外部サイトから取得して
引数とともに返します。
sqlite3_foo_init関数が外部に公開される拡張ライブラリ用のエントリーポイントです。
コンパイルされたファイルは"libfoo.so"(OSによっては.dylib/.dll)となります。
ではextensionをビルドしてみましょう。
% cargo build --example foo --release
ビルドに成功すると、target/release/examplesにlibfooファイルができてます。
% ls ./sqlite-plugin/target/release/examples/
foo-bf6dda611cc76dbe.d libfoo-bf6dda611cc76dbe.so libfoo.d libfoo.so
SQLiteのシェルから使う
sqlite3を起動してextensionをloadして使ってみます。
ちゃんと動いてますね。
% sqlite3 mydb.sqlite3
sqlite> .load target/release/examples/libfoo
sqlite> select foo("hello","world");
1st:hello, 2nd:world, ip:{"origin": "xxx.xxx.xxx.xxx"}
Nodeから使ってみる
プログラムからも使ってみます。
Node用SQlite3モジュールをインストールして、Nodeからアクセスしてみましょう。
% npm install sqlite3
↓のようにloadExtension関数を使って簡単に実装できます。
var sqlite3 = require('sqlite3');
//sqlite3データベースのパスを指定
let db = new sqlite3.Database("<your sqlite3 database file>", (err) => {
if (err) {
console.error(err.message)
throw err
}else{
console.log('Connected to the SQLite database.');
}
});
//libfooへのパスを指定
db.loadExtension('<path/your/target/release/examples/libfoo>');
db.all('select foo("hello","world")', (error, rows) => {
console.log(rows);
});
db.close();
rustqliteから使ってみる
Rustからもextensionにアクセスしてみます。
rusqliteを使えばさきほどと同じように実行可能です。
適当なRustプロジェクトを作成し、Cargo.tomlにrussqliteを記述します。
※load_extensionをfeaturesで記述すること
[dependencies]
rusqlite = { version = "0.29.0", features = ["bundled","load_extension"] }
Rustコードは↓のような感じです。
LoadExtensionGuardとload_extensionを使ってextensionをloadします。
unsafeを使ってるのはしょうがないところ。
use rusqlite::{Connection, Result,LoadExtensionGuard};
fn load_my_extension(conn: &Connection) -> Result<(),rusqlite::Error> {
unsafe {
let _guard = LoadExtensionGuard::new(conn)?;
//libfooへのパスを指定
conn.load_extension("path/your/target/release/examples/libfoo", None)
}
}
fn main() -> Result<(),rusqlite::Error>{
let conn = Connection::open_in_memory()?;
if let Ok(_value) = load_my_extension(&conn) {
let mut stmt = conn.prepare("select foo('hello','world')")?;
let mut rows = stmt.query([])?;
if let Some(row) = rows.next()? {
let res:String = row.get(0)?;
println!("{:?}",res);
}
} else {
println!("load_my_extension Err");
}
Ok(())
}
問題なく実行可能です。
% cargo run
"1st:hello, 2nd:world, ip:{\"origin\": \"xxx.xxx.xxx.xxx\"}"
Summary
というわけで、RustでSqliteのextensionを実装して使ってみました。
今回はシンプルな関数を実装しましたが、virtual table用のモジュールも作成できます。
virtual tableを使えばいろいろなオブジェクト(ex.csv,s3,kvsなど)を
テーブルとしてアクセスできるので、さらに便利に使えそうです。