【SQLite】コマンドラインでpythonユーザ定義関数を使えるようにしてみた(前編)
この記事について
この記事は、東京大学工学部電子情報工学科・電気電子工学科の学生実験「大規模ソフトウェアを手探る」のレポートとして作成したものです。
この実験では、実際に広く利用されている大規模ソフトウェアの動作を理解し、改良・機能拡張を行うことを目標として、学生がチームで課題に取り組みます。
私たちのチームは、よく知られているデータベース管理システム SQLite(エスキューライト)を対象とし、
「コマンドラインからユーザ定義関数を利用できるようにする機能追加」に挑戦しました。
本記事では、機能実装に至るまでの調査・解析と設計の過程をまとめています。
SQLite の機能変更、とくにシェルコマンド周辺の拡張に関心をお持ちの方のお役に立てば幸いです。
SQLite公式ホームページ: https://sqlite.org/
はじめに
この記事は、東京大学工学部電子情報工学科・電気電子工学科の学生実験「大規模ソフトウェアを手探る」のレポートの一部として書かれました。
こちらの記事では、sqlite3というソフトウェア自体やソースコードの構造を解説します。
コマンドラインからユーザ定義関数を使えるように機能拡張するための実装面についてご覧になりたい方は以下のリンクよりご覧ください。
また、レポートの全記事をご覧になりたい方は下のリンクよりご覧ください。
リポジトリ:
https://github.com/doss-eeic/2025-5-sqlite
背景・動機
SQLiteには以下のような特徴があります。
- サーバ不要で単一ファイルとして動作
- 設定が不要
- 高い移植性と信頼性
- 多言語・多ツールとの連携に強い
- JSONやビュー、トリガなど、実用的なRDBMSとしての機能を提供
参考:
https://qiita.com/ko1nksm/items/33ab7ced0f9f8acdff28
SQLiteを含む多くのデータベースでは、SQL文中で関数を利用することができます。
例:
select date('now') as date;(select [関数名](引数);)
一般的なデータベースでは、このような形式を用いてユーザ定義関数(UDF: User Defined Function)をロードすることが可能です。
SQLiteでも、CやPythonのAPIを介してUDFを登録することはできます。しかし、
SQLiteのコマンドライン(sqlite3シェル)上からUDFを直接定義することはできません。
SQLiteにおいて、コマンドライン上でUDFを使用する際には以下のような手順が必要です。
- CまたはPythonで関数を実装
- ソースをビルドして共有ライブラリ(
.so)を作成 -
.loadコマンドでロード
.load [ロードしたい共有ライブラリ]
しかし、ソースファイルをそのまま対象として UDF を登録する機能は存在しません。
そこで今回は、
- コマンドラインから直接 UDF を登録し、
- 以降は
select 関数名(引数);の形式で利用できるようにする
という機能を.load_py として実装することを目標としました。
以下は実行例です。
sqlite> .load_py multiply.py multiply my_mul
sqlite> select my_mul(3, 4);
12
使用技術 / 開発環境
| 技術 | 内容 |
|---|---|
| SQLite3 | 対象とするソフトウェア |
| Python 3.12 | Python/C API の使用 |
| Docker + Ubuntu | ビルドおよび開発環境 |
| VSCode Dev Containers | ソースコードの編集 |
| gdb | デバッガ |
SQLite3のビルド手順
ビルド方法は以下の資料を参考にしました:
https://doss.eidos.ic.i.u-tokyo.ac.jp/new_site/book/index.html
docker pull ubuntu
docker run -it --name sqlite-env ubuntu
docker start -ai sqlite-env
wget https://www.sqlite.org/2025/sqlite-autoconf-3500400.tar.gz
tar xvf sqlite-autoconf-3500400.tar.gz
cd sqlite-autoconf-3500400
./configure --prefix=<ソースファイルのディレクトリ>/inst CFLAGS="-O0 -g3"
make
make install
ソースコードの編集には、VSCodeのDev Containersという拡張機能を使いました。
コンテナ内でエディタを立ち上げずに、ホストOS側のVSCodeを使用してソースコードが編集できます。
ソースコードの構成
SQLite のディレクトリ構成は以下のようになっています。
sqlite-autoconf-3500400/
├─ inst/ # 自作のインストール用ディレクトリ
├─ sqlite3.c # SQLite 本体の機能
├─ shell.c # CLI の処理
├─ *.h # 種々のヘッダファイル
└─ Makefile # ビルド設定
SQLiteは、そのデータベースとしての機能のほとんどがsqlite3.cという単一のファイルに実装されています。一方、CLI(sqlite3シェル)は shell.c に実装されており、 今回の追加機能に関わる主要部分はこの shell.c に含まれています。
今回実装した .load_py は、CLI の拡張機能であるため、主に shell.c を中心に処理を追いました。
ソースコードの解析
SQLiteには、loadという、共有ライブラリファイル(.so, .dylibなど)をロードすることによってUDFを使えるようにするコマンドがある。そこで、
- 関数を追加するプロセスを確認するため、拡張SQL関数を loadコマンドによって読み込んだ際の挙動をデバッガ(gdb)で観察する
- ↑ で追加された拡張SQL関数実行時挙動を確認するため、select文から呼び出された際の挙動をデバッガ(gdb)で観察する
の2つの作業を行なった。
static int do_meta_command(char *zLine, ShellState *p){
...(中略)...
// インタラクティブシェル起動と画面表示
// 入力読み取り
if( c=='a' && cli_strncmp(azArg[0], "auth", n)==0 ){ //// 入力文字列を辞書順に照合
...(中略)...
if( c=='l' && cli_strncmp(azArg[0], "load", n)==0 ){
const char *zFile, *zProc;
char zErrMsg = 0;
failIfSafeMode(p, "cannot run .load in safe mode");
if( nArg<2 || azArg[1][0]==0 ){
/ Must have a non-empty FILE. (Will not load self.) */
eputz("Usage: .load FILE ?ENTRYPOINT?\n");
rc = 1;
goto meta_command_exit;
}
zFile = azArg[1];
zProc = nArg>=3 ? azArg[2] : 0;
open_db(p, 0);
rc = sqlite3_load_extension(p->db, zFile, zProc, &zErrMsg);
if( rc!=SQLITE_OK ){
shellEmitError(zErrMsg);
sqlite3_free(zErrMsg);
rc = 1;
}
}else
#endif