概要
PostgreSQLにはフロントエンドとしてCライブラリのlibpq
があります。モダンFortranのC相互運用機能を使って、FortranラッパーのLibpq-Fortran
を作ったのでこれの使い方を紹介します。
Libpq-Fortran — A Modern Fortran Interface for libpq
導入
関係データベース管理システムには色々な実装があります。例えばMySQL、PostgreSQL、IBM Db2などが思い浮かぶでしょう。他には軽量向けにSQLiteがあります。
SQLiteについてはいくつかFortranモジュールがあるようです。Philipp Engel氏によれば(参考文献1)、以下の4つのライブラリについてまとめられています。
fortran-sqlite3
Modern interface bindings to SQLite 3 in pure Fortran 2018.FLIBS
Fortran 90 modules that include non-standard wrapper routines around SQLitelibGPF
General Purpose Fortran collection, includes SQLite 3 bindings.sqliteff
SQLite for Fortran 2003, a thin C wrapper around the SQLite library.SQLite - Programming in Modern Fortran, written by Philipp Engel
しかしながら、これらはデータベースの「ライブラリ」であって、いずれもデータベース「サーバー」にアクセスする手段を提供するものではありません。導入のハードルは少々高いですが、それなりに大規模なデータを扱うならサーバーで管理したい場面もあると思います。
そのため、筆者はPostgreSQLデータベースサーバーへのクライアントインターフェースとして機能するFortranモジュールを開発することにしました。これは"Libpq-Fortran"という名前でGitHubで公開しています(MITライセンス)。
Libpq-FortranはFortranのC相互運用性の機能を使用して、PostgreSQLの公式Cライブラリlibpqへアクセスすることができます。このソフトウェアのドキュメントはまだ準備中ですが、本稿ではこれの最初の使い方について述べたいと思います。
依存関係ソフトウェア
このセクションではLibpq-Fortranが依存しているソフトウェアについて述べます。
ビルドに必要なソフトウェアは以下の通りです。
- libpq
- Fortranコンパイラ
- Fortranパッケージマネージャー(
fpm
)
まず、ローカルマシンのシステムにインストールされたlibpqライブラリが必要です。これはWindowsではPostgreSQLをウィザードでインストールしたときにインストールされます。具体的にはPostgreSQLバージョン15の場合、C:\Program Files\PostgreSQL\15\include\libpq-fe.h
というファイルが存在していれば大丈夫です。Ubuntuの場合はsudo apt install libpq-dev
のコマンドを実行すればインストールすることができます。
次にFortranコンパイラが必要になります。現在、Libpq-Fortranは以下のコンパイラによるビルドをサポートしています。
- GNU Compiler Collection:
gfortran
- Intel oneAPI HPC toolkit: Fortran Compiler
ifx
, Fortran Compiler Classicifort
更にFortranパッケージマネージャー(fpm
)が必要です。これはGitHubからダウンロードすることができます。加えて筆者の別ライブラリのuint-fortran
が必要ですが、これはfpm
が自動的に依存関係を処理してくれます。
最後に接続先のPostgreSQLデータベースサーバーが必要です。これはローカルマシンかローカルネット経由でアクセス可能である必要があります。このソフトウェアのテストに使うにはデータを失っても構わないデータベースを用意して使用してください。
ビルドする
手始めにLibpq-Fortranをビルドするには、bashやpowershellなどのターミナルから以下のコマンドを実行します。
$ git clone https://github.com/shinobuamasaki/libpq-fortran
$ cd libpq-fortran
$ fpm build
fpm build
を実行するときに、"libpq-fe.h"ファイルのディレクトリパスをコンパイラに教える必要がある場合があります。この場合は、環境変数FPM_CFLAGS
またはビルドオプション--c-flag
に-I
の後に続けてパスを記述します。
例えばUbuntuの場合、当該ディレクトリはおそらく/usr/include/postgresql
なので、コマンドは以下のようになります。
$ export FPM_CFLAGS="-I/usr/include/postgresql"
$ fpm build
または
$ fpm build --c-flag "-I/usr/include/postgresql"
Libpq-Fortranを試す
リポジトリのexample
ディレクトリには、対話的にPostgreSQLサーバーにアクセスして、サーバーのデータベース一覧を取得するプログラムが含まれています。
これを実行するには、fpm run
コマンドを以下のように実行します。
$ fpm run demo --example
demo.f90 done.
demo done.
[100%] Project compiled successfully.
=== INPUT Database Information ===
=== type "q" for quit ===
Hostname:
アクセスするサーバーについての情報入力を求めるHostname:
のプロンプトが表示されれば成功です。
これに次の例のように情報を入力すれば、データベースへの接続が確立されて結果を取り出すことができます。(ホスト名、データベース名、ユーザー名、パスワードは各自の環境に合わせて書き換えてください)。
=== INPUT Database Information ===
=== type "q" for quit ===
Hostname: localhost
dbname: postgres
user: shinobu
password: xxxyyyzzz
=== END INPUT ===
=== Query Result===
tuples, fields: 3 1
=== Available database names ===
postgres
template1
template0
Available database names
の下に少なくともpostgres
、template1
、template0
の3つが表示されれば成功です。これでFortranで記述されたコードを使って、データベースサーバーにアクセスすることを達成できました。次のセクションでは今使ったコードについて詳しく見ていきます。
program demo
このセクションでは上の例で用いたdemo.f90
のプログラムについて詳細に述べていきます。プログラムの全体は、本稿の補遺またはGitHubで見ることができます。
宣言部
このプログラムの宣言文は以下の通りです。
program demo
use :: libpq
use, intrinsic :: iso_c_binding
use, intrinsic :: iso_fortran_env, only:stdout=>output_unit, stdin=>input_unit
type(c_ptr) :: conn, res
character(:, kind=c_char), allocatable :: query, conninfo
integer :: i, port
character(256) :: str, host, dbname, user, password
ここで、use :: libpq
の宣言があることに注目してください。このモジュールはLibpq-Fortranの中核を担うモジュールです。Libpq-Fortranでは、このモジュールをuse
宣言することによってlibpqの手続をFortranから利用できるようになります。
次にuse, intrinsic :: iso_c_binding
の宣言があります。これを宣言しているのは、このソフトウェアがCライブラリの直接的なインターフェースなので、関数の戻り値としてオブジェクトのポインタを受け取ってそれをリレーしていく、というCスタイルのプログラミングが必要になるためです。type(c_ptr) :: conn, res
では、そのスタイルでプログラムを記述するために必要なCポインタ変数を宣言しています。
実行部
次に実行文について見ていきます。
print *, '=== INPUT Database Information ==='
print *, '=== type "q" for quit ==='
write (stdout, '(a)', advance='no') "Hostname: "
read (stdin, *) host
if (host == 'q') stop
port = 5432
write (stdout, '(a)', advance='no') "dbname: "
read (stdin, *) dbname
if (dbname == 'q') stop
write (stdout, '(a)', advance='no') "user: "
read (stdin, *) user
if (user == 'q') stop
write (stdout, '(a)', advance='no') "password: "
read (stdin, *) password
if (password == 'q') stop
print *, "=== END INPUT ==="
write(str, '(a, i0, a)') &
"host="//trim(adjustl(host))// &
" port=", port, &
" dbname="//trim(adjustl(dbname))// &
" user="//trim(adjustl(user))// &
" password="//trim(adjustl(password))
conninfo = str
この部分は、接続情報の入力を受け持つコードです。ユーザーの情報入力を固定長文字列に受け取って集め、最後にPostgreSQL接続文字列として1つの可変長文字列変数にコピーしています。
その次の実行文では、データベースへの接続を実行しています。
conn = PQconnectdb(conninfo)
if (PQstatus(conn) /= 0) then
print *, PQerrorMessage(conn)
error stop
end if
引数conninfo
を与えて関数PQconnectdb
を呼び出し、結果をCポインタ型変数conn
に代入しています。このポインタは接続情報の識別子としてユーザーが使用しますが、このオブジェクトの内部構造について考える必要はありません。これに続くif
ブロックはエラー処理です。接続を確立できなかった場合にエラーメッセージを表示してプログラムを終了します。
更にすすむとSQL文を実行するコードが出てきます。
query = "select datname from pg_database;"
res = PQexec(conn, query)
if (PQstatus(conn) /= 0 ) then
print *, PQerrorMessage(conn)
end if
ここではまず、文字列変数query
にSQL文を書き込みます。その後、これとconn
を引数に与えて関数PQexec
を呼び出し、結果をCポインタ変数res
に格納します。このSQL文の意味するところは「サーバーにある(テーブルの集合という意味での)データベースの一覧からデータベース名を取得する」というものです。これに続くif
ブロックは、先程と同様にエラーハンドリングです。
そして、res
オブジェクトから、結果のデータを取り出す部分に移ります。
print *, "=== Query Result==="
print '(a, i0, 2x, i0)', 'tuples, fields: ', PQntuples(res), PQnfields(res)
print *, "=== Available database names ==="
do i = 0, PQntuples(res)-1
print *, PQgetvalue(res, i, 0)
end do
まず関数PQntuples
とPQnfields
を呼び出して、res
オブジェクトがもつ結果の行数と列数を表示します。次に行数だけループを繰り返して、関数PQgetvalue
でi
行0列目のデータを取り出して表示します。ここでC言語のルールに従って0-basedのインデックスが使われていることに注意してください。
最後にPQclear
関数とPQfinish
関数を呼び出して結果のオブジェクトを解放し、接続を終了しています。
call PQclear(res)
call PQfinish(conn)
実行結果
この通りに書かれたプログラムを実行すると、上で実行したようにターミナルには次のような表示が出力されるでしょう。
=== Query Result===
tuples, fields: 3 1
=== Available database names ===
postgres
template1
template0
ここで少なくとも(PostgreSQLサーバーのデフォルトの設定では)postgres
、template1
、 template0
の3つのデータベース名が表示されていれば成功です。
おわりに
PostgreSQLの公式ライブラリlibpqへアクセスするためのFortranモジュールLibpq-Fortranを公開し、その最初の使い方について紹介しました。
Libpq-Fortranはlibpqをそのままラップしただけのインターフェースなので、実際にアプリケーションのデータを管理するにはSQLと多少のデータベース設計の知識が必要です。特に数値計算のデータ保存をPostgreSQLデータベースを用いて実現するための方法は、本稿で書くには長くなりすぎるので、別の記事で書きたいと思います。
謝辞
-
参考文献1は@cure_honeyさんに教えていただきました。ありがとうございます。
-
このライブラリを作ったのは、2023年9月10日のモダンFortran勉強会.forでのディスカッションがきっかけの一つです。
参考文献
- SQLite - Programming in Modern Fortran, written by Philipp Engel
補遺
demo.f90
のプログラム全体は次の通りです。
program demo
use :: libpq
use, intrinsic :: iso_c_binding
use, intrinsic :: iso_fortran_env, only:stdout=>output_unit, stdin=>input_unit
type(c_ptr) :: conn, res
character(:, kind=c_char), allocatable :: sql, conninfo
integer :: i, port
character(256) :: str, host, dbname, user, password
print *, '=== INPUT Database Information ==='
print *, '=== type "q" for quit ==='
write (stdout, '(a)', advance='no') "Hostname: "
read (stdin, *) host
if (host == 'q') stop
port = 5432
write (stdout, '(a)', advance='no') "dbname: "
read (stdin, *) dbname
if (dbname == 'q') stop
write (stdout, '(a)', advance='no') "user: "
read (stdin, *) user
if (user == 'q') stop
write (stdout, '(a)', advance='no') "password: "
read (stdin, *) password
if (password == 'q') stop
print *, "=== END INPUT ==="
write(str, '(a, i0, a)') &
"host="//trim(adjustl(host))// &
" port=", port, &
" dbname="//trim(adjustl(dbname))// &
" user="//trim(adjustl(user))// &
" password="//trim(adjustl(password))
conninfo = str
conn = PQconnectdb(conninfo)
if (PQstatus(conn) /= 0) then
print *, PQerrorMessage(conn)
error stop
end if
! The query to retrieve the names of databases within a database cluster is:
sql = "select datname from pg_database;"
res = PQexec(conn, sql)
if (PQstatus(conn) /= 0 ) then
print *, PQerrorMessage(conn)
end if
print *, "=== Query Result==="
print '(a, i0, 2x, i0)', 'tuples, fields: ', PQntuples(res), PQnfields(res)
print *, "=== Available database names ==="
do i = 0, PQntuples(res)-1
print *, PQgetvalue(res, i, 0)
end do
call PQclear(res)
call PQfinish(conn)
end program demo