Yet another OSS DB:Firebird(2)

データベースファイルの内部構造を探ってみた

 

GlinkのソースコードからFirebirdの実装を見る

 今回はこれをDelphiでリライトして、多少機能を追加してみました。

 変更したのは、新しく指定する2次データベースファイルの存在チェックと絶対パスであるかどうかのチェック、-Hオプションで表示されるヘッダ情報にPAGESとnext_pageを追加、2次データベースファイル以降チェックサムエラーが表示される不具合の修正、ヘッダの表示をBasicPageHeaderとそのほかを分けて見やすくした、などです。

 ソースコードはそれぞれ以下のリンク先から取得できます。

Glinkの実装を追いかける

 まず、ParamCountから引数をパースするかどうかを判断しています。

 グローバルな変数の初期化もこの中で行っているので、引数がなくても取りあえず呼ぶようになっています。起動すると引数に従って、グローバル変数sw_show_versionsw_show_helpsw_show_headersw_show_header_full が設定され、その後の動作が決定されます。

 ページヘッダの内容は、get_db_header() 関数で取得され、hdr_page で宣言されたレコード型の変数header_page に読み込まれます。このときに、まずMIZ_PAGE_SIZE=1024bytes を読み込んでから、再度header_page.fix_data.hdr_page_size の大きさで読み込んでいます。可変長のページサイズを処理するためにこのような手続きを取っています。

謎の数値40587を探る

 show_db_header() 関数の中では、このheader_page の各フィールドを書式化して表示しています。Firebirdの内部時刻型は日付部分を32ビット整数で、時刻部分を同じく32ビット整数で格納しています。これをCのtime_t型に変換するためにIvan氏が書いたコードが以下の部分です。

Firebirdの内部時刻型をtime_t型に変換する(Glink.exeソースコード424〜427行目)
ti = 86400 * (header_page.fix_data.hdr_creation_date[0] - 40587) + _timezone
   + (header_page.fix_data.hdr_creation_date[1] / 10000);
if (_daylight)
ti = ti - 3600;

 hdr_creation_date[0]から40587を引いた値に86400(1日の秒数)を乗じているのは、これが日付部分であるためですが、40587はどこから来ているのでしょうか。

バイナリエディタでささっと調査

 いろいろと調べてみればどこかに書いてあるのでしょうが、面倒なのでバイナリエディタでhrd_creation_date[0][1]をそれぞれ0で書き換えてみました。結果、1858/11/17 00:00:00 が起点となっていることが分かりました。40587を365で割ると約111年になりますから、time_t型の1970/1/1 00:00:00からの経過日数との差がこの数値になっていることが分かります。ちなみに、FirebirdのTimeStamp型自体は西暦100年から32768年まで対応しています。

 Delphiでは日付時刻型は整数部が1899年12月31日からの経過日数で、時刻部分は小数部で表現されています。そのため、以下のような変換を行いました。また、time_t型との日付差を修正するため、Delphiに組み込みのUnixDateDelta 定数を利用しました。

t_time型との日付差に対応するための修正
ti := (header_page.fix_data.hdr_creation_date[0] - 40587);
ti := incsecond(ti, trunc(header_page.fix_data.hdr_creation_date[1] / 10000));
if (_daylight > 0) then ti := incsecond(ti, -3600);
ti := ti + UnixDateDelta;

 本来は、ibase.h で定義されている、isc_decode_date() 関数を利用するのが本筋なのでしょうが、内部格納形式を理解する一助にはなるかと思います。もっとも、今後内部格納形式が変更された場合には、APIルーチンはそれを考慮して正しい値を返すはずですが、上述の方法では対応できないことはいうまでもありません。

clumpletsの実装チェックと置き換え

 さて、struct hdrの最終バイトであるUCHAR hdr_data[1]以降にはclumpletsと呼ばれる可変長のデータが格納されています。以下にods.hの定義を示します。

ods.hでのclumpletsの定義
/* Header page clumplets */
/* Data items have the format
 <type_byte> <length_byte> <data...>
*/

#define HDR_end 0
#define HDR_root_file_name 1    /* Original name of root file */
#define HDR_journal_server 2    /* Name of journal server */
#define HDR_file 3              /* Secondary file */
#define HDR_last_page 4         /* Last logical page number of file */
#define HDR_unlicensed 5        /* Count of unlicensed activity */
#define HDR_sweep_interval 6    /* Transactions between sweeps */
#define HDR_log_name 7          /* replay log name */
#define HDR_journal_file 8      /* Intermediate journal file */
#define HDR_password_file_key 9 /* Key to compare to password db */
#define HDR_backup_info 10      /* WAL backup information */
#define HDR_cache_file 11       /* Shared cache file */
#define HDR_max 11              /* Maximum HDR_clump value */

 clumpletsのタイプが1、2、3、7、11のときは文字列型なので、length_byteの長さに従って文字列を取り出しています。また、8、9、10は現在使用されていないため除外しています。4、5、6の場合は整数型になるのですが、Ivanさんのコードでは後ろから前へ1byteずつ取り出してInteger型に加算しては左へ8 ビットシフトしていました。数値型の値が4bytesまでで入っているという仮定では、これは合理的なやり方です。

Glink.exeソースコードでのclumplets操作の実装
 int i, j, k, len;
for (j = 0, k = len;
k--;
j = (j << 8) + header_page.var_data[i+k+2]);
printf (str, j);

 ひとまず、そのままDelphiのコードに置き換えてると以下のようになります。

clumplets操作部分の移植
i,j,k,len:integer;
j := 0; k := len;
repeat
 dec(k);
 j := (j shl 8) + header_page.var_data[i+k+2];
until (k = 0);
writeln(format(str, [j]));

 Cに比較すると長くなってしまうのは致し方ないですが、もっとうまい書き方があるかもしれません。しかし、今回は数値型のデータにはすべて4bytesで入っているようなので、以下のように書き換えることもできます。本来的には可変長なので、lenに応じてbyte、word、integerで受けるようにしないといけませんが、もしlenが3だったらbyteのarrayを作って対応するしかないので、やはり上記の方法が正解なのでしょう。

整数型がすべて4bytesの場合の別の移植方法
i,k,len:integer;
j:^integer;
j := @header_page.var_data[i+2];
writeln(format(str, [j^]);

update_db_header()

 次に、データベースファイルのリンク先を書き換えているupdate_db_header() 関数ですが、ここではnew_header_pagefix_dataheader_page.fix_data をコピーしてから、clumpletsを1つずつコピーしていって、type_byteが3のときだけリンク先を変更して書き換えを行っています。その後、書き換えがうまくいっていれば、データベースファイルの先頭へpage_size分を書き込んで完了となります。

おわりに

 今回は、DelphiからFirebirdのデータベースファイルを直接操作してみました。本当に簡単な紹介だけとなりましたが、データベースファイルの内部構造に触れることで、Firebirdへの理解が少しでも深まっていただけたならなによりです。今後は、さらにデータが実際にどのように格納されているのか、インデックスはどのように格納されているのかなど、踏み込んだ内容をお伝えできればと考えています。

前のページへ 3/3  

Index
Yet another OSS DB:Firebird(2)
データベースファイルの内部構造を探ってみた
    Page 1
・1つのバイナリファイル
   Page types
   Firebirdのページサイズ
   ページサイズの選択肢が多い理由
   メモリサイズの制限
   ページタイプの種類と目的
   Page 2
・ページファイルの中身(Basic page header)
・Glink
→ Page 3
・GlinkのソースコードからFirebirdの実装を見る
   Glinkの実装を追いかける
   謎の数値40587を探る
   バイナリエディタでささっと調査
   clumpletsの実装チェックと置き換え
   update_db_header()


Yet another OSS DB:Firebird



Database Expert フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Database Expert 記事ランキング

本日月間