0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SQLite】コマンドラインでpythonユーザ定義関数を使えるようにしてみた(後編)

Last updated at Posted at 2025-11-20

この記事について

この記事は、東京大学工学部電子情報工学科・電気電子工学科の学生実験「大規模ソフトウェアを手探る」のレポートの一部として書かれました。
こちらの記事では、sqlite3のコマンドラインからユーザ定義関数を使えるように機能拡張するプロセスのうち、特にその実装面についてご紹介します。ソフトウェア自体やソースコードの構造に関する紹介をご覧になりたい方は以下のリンクよりご覧ください。

また、レポートの全記事をご覧になりたい方は下のリンクよりご覧ください。

追加したい機能

「前編」において述べた通り、sqlite3ではコマンドラインからユーザ定義関数(UDF)を定義し、select文によって使用できるようにする、という機能が存在しません。私たちはオープンソースとして公開されているsqlite3のソースコードを編集し、この機能を追加することを目標としました。
具体的な仕様は以下に示す通りです。

specification
$ .load_udf  [ソースファイル名]   [ソースファイル内の関数名]   [SQL関数として登録したい名前]
$ select [上で登録したUDF名] (引数);	// 以降はloadせずに直接使用可

すなわち、一度 "load_udf"コマンドによってUDFを定義すれば、そのデータベースとの接続を終了するまでの間、select文によってその関数を(デフォルトのSQLite関数と同様に)使用することができる、という仕組みです。

機能追加の方針

  • CのUDFの場合
    一般に、Cで記述されたユーザ定義関数をsqlite3で使用したい場合には、そのソースファイルを共有ライブラリ(.soや.dll形式のファイル)としてコンパイルし、.loadコマンドによって読み込むという方法があります。したがって、Cに対してload_udfを実装する場合、ソースファイルを引数として受け取り、コンパイルからライブラリの読み込みまでを一括して行う機能を持たせる方針を考えました。
    しかし、後になってこの方針は困難であることが判明しました。sqlite3で.loadコマンドを実行すると、内部的にはsqlite3_load_extension関数によって読み込みが行われます。この関数の仕様として、UDFをsqliteに登録するためには、読み込み先の共有ライブラリにしかるべき初期化関数がエントリポイントとして要求されるようです。 1任意の構造を持つCソースファイルに対し、この初期化関数を自動的に生成・挿入するのは困難(しかも非本質的)であることから、今回は実装を断念しました。

(参考)sqlite3_load_extensionの仕様

  • PythonのUDFの場合
    Cソースファイルから直接UDFを登録するのが困難である一方、コンパイル不要で実行できるPythonのUDFの場合には比較的容易に実装が可能であると考えました。具体的にはPython/CのAPIを利用してPythonインタプリタを起動し、実行用に追加したCのラッパー関数に対して、実行したいPython関数から作成されたPythonオブジェクトを渡すという方法です。

したがって、今回の実験ではPythonのUDFに対するload_udfコマンド(以下load_py と呼ぶ)を実装する方針で取り組みました。

実装

load_pyの実装のため、sqlite3に以下の2つの機能を追加しました。

  • sqlite3_create_function_v2による関数登録
    まず、「前編」にて紹介したdo_meta_command関数における辞書順の文字列照合に"load_py"に対する判定を追加し、Pythonインタプリタの起動とデータベース接続を行います。ただし、処理を簡潔にするため、一度目に起動させたPythonインタプリタはデータベース接続が終了するまで閉じずに開き続けます。 2
    続いて、sqlite3_create_functionによって関数を登録します。具体的な構成は以下の通りです。sqlite3の公式ページにsqlite3_create_functionの詳細な仕様が掲載されているため、それを参考に実装しました。今回はそのうち特にsqlite3_create_function_v2を使用しました。
create_function
sqlite3_create_function_v2(
    db, // 接続先データベース
	zName, 	// 登録するSQL関数の名前
	-1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, // テキストエンコーディング
	callable,	 // 実行用のC関数に渡すpythonオブジェクト
	exec_python_inC, 	// 実行用のCラッパー関数
	NULL, NULL, 
	destroy_pyfunc 		// pFuncのデストラクタ
);

Python/CのAPIによってCの関数中にPythonの機能を埋め込む場合、通常のPythonにおいて自動的に行われているメモリ管理を手動で実装しておかねばなりません。すなわち、使い終わったPythonオブジェクト等は必ず手動でメモリ解放する必要があります。 3データベース接続中は一度登録したUDFを維持する仕様としたいことを考慮すると、今回の場合は接続が終了する直前でPythonオブジェクトの解放を行う関数(上記コード中のdestroy_pyfunc)が実行されればよいことになります。sqlite3_create_function_v2は引数としてこのPythonオブジェクトのデストラクタを与えられる仕様になっているため、今回使用しました。

(参考)sqlite3_create_functionの仕様

  • 実行用のCラッパー関数の追加
    上のsqlite3_create_function_v2で登録している関数の実体は、正確にはPythonオブジェクトではなくCのラッパー関数(exec_python_inC)です。このラッパー関数にcreate_functionによって登録されたPythonオブジェクトが渡され、sqlite3からpythonへの型変換を経てPython/CのAPI(PyObject_CallObject)によって実行される仕組みになっています。

ソースコード

pyfunc_ext
#include "pyudf.h"

// 実行用のラッパー関数
void exec_python_inC(
	sqlite3_context *context,
	int argc,
	sqlite3_value **argv
	){
	PyObject *pFunc, *pArgs, *pValue, *pResult;
	int i;

	pFunc = (PyObject *)sqlite3_user_data(context);
	if (pFunc == NULL || !PyCallable_Check(pFunc)){
		sqlite3_result_error(context, "failed to call python function", -1);
		return;
		// pFuncの参照はdestroy_pyfuncにて減らすのでここではDECREFしない
	}

	pArgs = PyTuple_New(argc);
	if (pArgs == NULL) {
		sqlite3_result_error(context, "failed to create python tuple", -1);
		return;
	}

	for (i = 0; i < argc; ++i) {
		// 引数の型の判別
		int nType = sqlite3_value_type(argv[i]);
		if (nType == SQLITE_INTEGER){
		// sqlite3の整数からpythonの整数へと変換
		pValue = PyLong_FromLongLong(sqlite3_value_int64(argv[i]));
		if (!pValue) {
			Py_DECREF(pArgs);
			sqlite3_result_error(context, "failed to convert argument to python int", -1);
			return;
		}
		} else {
		Py_DECREF(pArgs);
		sqlite3_result_error(context, "invalid argument type", -1);
		return;
		}

		// pArgsがpValueの参照を盗む仕様になっているため、pValueのDECREFは必要ない
		PyTuple_SetItem(pArgs, i, pValue);
	}

	// 実行
	pResult = PyObject_CallObject(pFunc, pArgs);
	Py_DECREF(pArgs);

	if (pResult != NULL) {
		// pResultの型に応じてsqlite3からpythonへと型変換
		if (PyLong_Check(pResult)) {
		sqlite3_result_int64(context, PyLong_AsLongLong(pResult));
		} else {
		sqlite3_result_error(context, "invalid argument type", -1);
		}
		Py_DECREF(pResult);
	} else {
		PyErr_Print();
		sqlite3_result_error(context, "failed to call python", -1);
	}
}

// Pythonオブジェクトのメモリ解放
void destroy_pyfunc(void *pUserData){
	PyObject *pFunc = (PyObject *)pUserData;
	if (pFunc){
		Py_DECREF(pFunc);
	}
}

// sqlite3_create_function_v2に関連するヘルパ関数

int register_pyfunc(sqlite3 *db, const char *zName, int nArg, PyObject *callable){
	if (!db || !zName || !callable || !PyCallable_Check(callable)) {
		return SQLITE_MISUSE;
	}

	Py_INCREF(callable);

	int rc = sqlite3_create_function_v2(
		db, // データベース
		zName, // 作成するSQL関数の名前
		-1, // 引数の個数(-1とすると任意)
		SQLITE_UTF8 | SQLITE_DETERMINISTIC, // テキストエンコーディング
		callable, // 実行用のC関数に渡すpythonオブジェクト
		exec_python_inC, // 実行用のC言語関数
		NULL, 
		NULL, 
		destroy_pyfunc // pFuncのデストラクタ
	);

	if (rc != SQLITE_OK){
		Py_DECREF(callable);
	}
	return rc;
}

結果

今回はサンプルとして、掛け算を行い結果を表示する極めて単純なプログラム(multiply.py)を使用します。

multiply.py
def multiply(a,b):
    print("Will compute", a, "times", b)
    c = a * b
    return c

まず、何もせずにmy_multiplyという関数をselect文で実行してみます。

sample
sqlite> select my_multiply(2,3);
Parse error: no such function: my_multiply
  select my_multiply(2,3);
         ^--- error here

すると、上記のようにno such functionというエラーが出ました。それでは、今回実装したload_pyコマンドによってユーザ定義関数を登録してから同様に実行してみます。

sample
sqlite> .load_py multiply.py multiply my_multiply;
sqlite> select my_multiply(2,3);
Will compute 2 times 3
6

今度は掛け算の結果が正しく出力されました!これにより、無事PythonのUDFを登録するのに成功したことが分かります。

あとがき

今回は時間の都合により、整数型以外の処理への対応は行いませんでした。機会があればそのほかの型にも対応した実装を行いたいです。また、今回はmultiply.pyというごく簡単なプログラムでテストを行いましたが、デフォルトのsqliteに存在しない便利な機能をpythonで記述してこそ意義のある機能拡張になると考えます。コマンドラインからデータベースを操作するという状況そのものがいささか限定的には思われますが、どのようにしてこのUDF登録の機能を有意義に利用するか、という点についても引き続き考察したいと考えています。

  1. 最初にsqlite3_extension_initという名前の関数が検索され、見つからない場合にはファイル名(例: mylib.so)から推測した名前(例: sqlite3_mylib_init)が検索される。

  2. データベース接続の終了に伴い、このメモリ領域はシステムによって自動的に回収されるためメモリリークの危険性はほとんどゼロだと考えられる。

  3. この場合、メモリ解放とは参照カウントを減らす(=py_decref する)ということ。同様に関数登録の際には参照カウントを増やし(=py_incref)している。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?