ククログ

株式会社クリアコード > ククログ > PGroongaに関数を追加する(C拡張編)

PGroongaに関数を追加する(C拡張編)

PGroongaとPostgreSQLについてお勉強中の阿部です。

先日、PGroongaに便利関数を追加したのでその方法について説明します。 今回はC言語で集合を返す関数を実現する方法です。 主な処理をC言語で実装して、CREATE FUNCTION で関数を追加するかたちです。

PGroongaはPostgreSQLの拡張機能です。 PostgreSQLの拡張機能を1から開発する方法には触れませんが、これから拡張機能を開発する方の参考になると思います。

概要

この記事では「C言語関数」を開発するときのアウトラインのみ説明します。

一部、実際に開発したpgroonga_list_broken_indexes 関数のコードを例にしますが、仕様の詳細については触れません。 関数の中身ではなく「C言語関数」を開発するときの定型作業の説明をメインにしたいためです。

C言語でPostgreSQLの関数を実装する際は、「C言語での実装」と「C言語で実装した関数をCREATE FUNCTIONで関数を宣言する」が必要です。

特に「C言語での実装」を「スケルトンの作成」、「本体の実装」の2つステップに分けて説明します。

スケルトンの作成

まずは何も返さないC言語関数を実装するところまでを説明します。

具体的には以下を行います。

  • C言語関数のスケルトンを追加
  • ビルドの設定
  • CREATE FUNCTION
  • ユニットテストの追加
    • (本文が長くなるのでユニットテストの追加については割愛)

参考PR: https://github.com/pgroonga/pgroonga/pull/432/files

C言語関数のスケルトンを追加

コードを掲載してポイントを簡単に説明します。

#include "pgroonga.h"

#include <funcapi.h>

PGDLLEXPORT PG_FUNCTION_INFO_V1(pgroonga_list_broken_indexes);

Datum
pgroonga_list_broken_indexes(PG_FUNCTION_ARGS)
{
	FuncCallContext *context;
	if (SRF_IS_FIRSTCALL())
	{
		context = SRF_FIRSTCALL_INIT();
	}
	context = SRF_PERCALL_SETUP();
	// todo
	SRF_RETURN_DONE(context);
}

以下の3つに分けて説明します。

  • PGDLLEXPORT PG_FUNCTION_INFO_V1(pgroonga_list_broken_indexes);
    
  • Datum
    pgroonga_list_broken_indexes(PG_FUNCTION_ARGS)
    
  • 本体部分

PGDLLEXPORT PG_FUNCTION_INFO_V1(pgroonga_list_broken_indexes);

pgroonga_list_broken_indexes は実装する関数名です。それ以外の部分はこのように書くことになっているので、 とりあえずこのように書いておけば良いです。

PGDLLEXPORT で明示的にエクスポートして、PG_FUNCTION_INFO_V1() で「Version 1 呼び出し規約」をサポートしていることを示します。

参考: https://www.postgresql.jp/document/16/html/xfunc-c.html#XFUNC-C-V1-CALL-CONV

Datum pgroonga_list_broken_indexes(PG_FUNCTION_ARGS)

この部分もC言語関数を実装するときはこのように書くことになっているので、このように書いておけば良いです。

pgroonga_list_broken_indexes が関数名で CREATE FUNCTION するときはこの名前で指定します。

Datum はPostgreSQLでデータを扱うための便利な型です。

本体部分

このドキュメントに詳しく書いてあるので、 それを読むのが一番ですが、ポイントだけ説明します。 このあとの説明でドキュメント内の用語を少し使うので、簡単に目を通してから読むとより良いです。

今回実装した関数は「ValuePerCallモード」です。 この関数の面白いところは「集合を返す関数が繰り返し呼び出され(毎回同じ引数を渡します)、返す行がなくなるまで呼び出しごとに1つの新しい行を返し、返す行がなくなったらNULLを返す」ところでしょうか。 結果が10行があったら10回呼び出されるということです。

ということで、同じ関数が何度も呼び出させるので、何回目の呼び出しなのかなどの状態をいい感じで管理してくれるのが FuncCallContext です。

利用するための便利マクロがあるのでそれを使って、初期化や片付けなどを行います。

補足: コードに登場する SRF は「集合を返す関数(Set Returning Function)」のことです。

ビルドの設定

今回は新しいソースファイルを追加しました。 そのファイルがビルド対象になるように、追加したファイルを追記します。

PGroongaでは makefiles/pgroonga-sources.mk に追加します。

 	src/pgrn-index-status.c			\
 	src/pgrn-jsonb.c			\
 	src/pgrn-keywords.c			\
+	src/pgrn-list-broken-indexes.c		\
 	src/pgrn-match-positions-byte.c		\
 	src/pgrn-match-positions-character.c	\
 	src/pgrn-normalize.c			\

CREATE FUNCTION

CREATE FUNCTION 文のみ紹介します。 更新すべきファイルなどはこちらをご確認ください。

CREATE FUNCTION pgroonga_list_broken_indexes()
	RETURNS void /* todo */
	AS 'MODULE_PATHNAME', 'pgroonga_list_broken_indexes'
	LANGUAGE C
	STRICT
	PARALLEL SAFE;
  • RETURNS void
    • void で何も返さない関数の指定
  • AS 'MODULE_PATHNAME', 'pgroonga_list_broken_indexes'
  • LANGUAGE C
    • C言語関数なので LANGUAGEC を指定

中締め

ここまでの作業が完了し、ビルドも終わると、以下のSQLが実行できるようになります。

SELECT * FROM pgroonga_list_broken_indexes();

(ここまでではメインの処理を何も実装していないので結果は空ですが)

とりあえず実装したC言語関数が動いたのを確認できたら、本体の実装に移ります。

本体の実装

pgroonga_list_broken_indexes() のコードを一部抜粋して、 「C言語関数を実装するときはだいたいこういう感じでやります」というよくあるパターンの説明をします。

参考PR: https://github.com/pgroonga/pgroonga/pull/433/files

Datum
pgroonga_list_broken_indexes(PG_FUNCTION_ARGS)
{
	...
	while ((indexTuple = heap_getnext(data->scan, ForwardScanDirection)))
	{
		// nameはPGroongaのインデックス名
		SRF_RETURN_NEXT(context, name);
	}
	...
}

ほぼ中身がなくなってしまいましたが、伝えたいのは SRF_RETURN_NEXT()です。 pgroonga_list_broken_indexes はインデックス名のみを返すので、name にインデックス名を設定して、 SRF_RETURN_NEXT() で呼び出し側に返しています。

これで値を返すようになったので、CREATE FUNCTION も更新する必要があります。

 CREATE FUNCTION pgroonga_list_broken_indexes()
-	RETURNS void /* todo */
+	RETURNS SETOF text
 	AS 'MODULE_PATHNAME', 'pgroonga_list_broken_indexes'
 	LANGUAGE C
 	STRICT

ここまで完了して以下のSQLを実行するとPGroongaのインデックスが表示されます。

SELECT * FROM pgroonga_list_broken_indexes();

参考: 複数の列を返すとき

pgroonga_list_broken_indexes は1列しか返さなかったので、複数列を返す場合の例も掲載します。 pgroonga_wal_status() のコードが参考になります。

参考: https://github.com/pgroonga/pgroonga/blob/3.2.1/src/pgrn-wal.c#L3077-L3192

ポイントのコードだけ抜粋して簡単に説明します。

まず、タプルを設定します。

if (SRF_IS_FIRSTCALL())
{
	...
	data->desc = CreateTemplateTupleDesc(nAttributes);
	TupleDescInitEntry(data->desc, 1, "name", TEXTOID, -1, 0);
	TupleDescInitEntry(data->desc, 2, "oid", OIDOID, -1, 0);
	TupleDescInitEntry(data->desc, 3, "current_block", INT8OID, -1, 0);
	TupleDescInitEntry(data->desc, 4, "current_offset", INT8OID, -1, 0);
	TupleDescInitEntry(data->desc, 5, "current_size", INT8OID, -1, 0);
	TupleDescInitEntry(data->desc, 6, "last_block", INT8OID, -1, 0);
	TupleDescInitEntry(data->desc, 7, "last_offset", INT8OID, -1, 0);
	TupleDescInitEntry(data->desc, 8, "last_size", INT8OID, -1, 0);
	BlessTupleDesc(data->desc);
	...
}

これは初回の実行時のみ実行されます。

そして、返したい値をタプルに値を設定して、

...
values[i++] = Int64GetDatum(currentBlock);
values[i++] = Int64GetDatum(currentOffset);
values[i++] = Int64GetDatum(currentBlock * BLCKSZ + currentOffset);
{
	BlockNumber lastBlock = 0;
	LocationIndex lastOffset = 0;
	if (PGrnWALEnabled)
		PGrnWALGetLastPosition(index, &lastBlock, &lastOffset);
	values[i++] = Int64GetDatum(lastBlock);
	values[i++] = Int64GetDatum(lastOffset);
	values[i++] = Int64GetDatum(lastBlock * BLCKSZ + lastOffset);
}
...

SRF_RETURN_NEXT() で値を返します。

...
HeapTuple tuple = heap_form_tuple(data->desc, values, nulls);
SRF_RETURN_NEXT(context, HeapTupleGetDatum(tuple));
...

まとめ

PGroongaにC言語で実装した関数を追加する方法のアウトラインを説明しました。

今回は触れませんでしたが、ユニットテストの追加も必要です。ユニットテストもあるとメンテナンス性が向上します。 次回はPGroongaのユニットテストについて説明します。乞うご期待!

SQLのみで関数を追加する方法もまとめたのでご覧ください。