それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

MySQL 8.0.18 の実装を読み解きながら簡単なストレージエンジンを自作する

はじめに

卒論書くのに飽きてきて何かやりたくなったので急にストレージエンジンを書くことにしてみた。
MySQL のストレージエンジンを実装していく中で、色々できるかなと思っていたけど、やってみると MySQL の内部実装について色々知らないといけないことが多くインデックスとかトランザクションとかそういうところは実装できなかった。

github.com

MySQL をビルドする

ストレージエンジンを開発するためにはまずソースコードをビルドする必要がある。

$ cd /tmp
$ wget  https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.18.tar.gz
$ tar xzvf mysql-boost-8.0.18.tar.gz

ビルドに必要なパッケージを引っ張ってくる。

$ sudo apt-get install -y cmake libncurses5-dev libssl-dev build-essential

ビルドする。

$ cd mysql-8.0.18/
$ cmake . -DWITH_BOOST=./boost -DFORCE_INSOURCE_BUILD=1
$ make

ビルドまでしたら次は make install をするべきなのだけど、ストレージエンジンのプラグインは make すれば共有ライブラリ(.so) として吐かれるため
それを MySQL の docker コンテナ内に突っ込んで適用する。

ストレージエンジンを自作する

Example エンジンをベースにする

MySQLのストレージエンジンを自作してみる - 備忘録の裏のチラシ
↑この記事と

第84回 ストレージエンジンをビルドしてみる:MySQL道普請便り|gihyo.jp … 技術評論社
この記事を参考にしている。

まず storage ディレクトリ以下にストレージエンジンのコードがエンジン別のディレクトリに分かれているので作成する。
ここでは、example エンジンをベースにする。

$ cd mysql-8.0.18
// cp -R storage/example/ storage/gambit ここで自分は gambit という名前にしている
$ cp -R storage/example/ storage/{好きな名前}

ここまでやると storage/gambit が次の様な構成になっているはず。

mysql-8.0.18/storage/gambit$ ls
CMakeFiles      CTestTestfile.cmake  cmake_install.cmake  ha_example.h
CMakeLists.txt  Makefile             ha_example.cc

このあとが少し面倒で、コピーした example エンジンのコード内にある example, EXAMPLE を gambit, GAMBIT に修正する必要がある。

まず修正するのは Makefile
Makefile 内の example -> gambit に置換する。

次に CMakeList.txt をひらいて、 example -> gambit, EXAMPLE->GAMBIT に置換する。
それが終わったら ha_example.* を ha_gambit.* に置換して、そのコード内の Example, EXAMPLE, example を置換する。

ここまでおわったら MySQL のプロジェクトルートに cd して

$ cmake . -DWITH_BOOST=./boost -DFORCE_INSOURCE_BUILD=1
$ make

これで再度適用して、 show engines で該当するストレージエンジンがあれば問題ない。
ストレージエンジンのコードを修正した場合は、make すれば、 plugin_output_directory にビルドされる。

handlerton の作成とインスタンス

handlerton (handler singleton の略らしい)は、ストレージエンジンの定義と諸々の処理に対するメソッドポインタを持っている。
これを作成し、インスタンス化することで MySQL はストレージエンジンを使用することができる。
また、ストレージエンジンのハンドラを作成しているのは

handlerton *gambit_hton
static int gambit_init_func(void *p) {
  DBUG_TRACE;

  gambit_hton = (handlerton *)p;
  gambit_hton->state = SHOW_OPTION_YES;
  gambit_hton->create = gambit_create_handler;
  gambit_hton->flags = HTON_CAN_RECREATE;
  gambit_hton->is_supported_system_table = gambit_is_supported_system_table;

  return 0;
}

に含まれる gambit_hton->crate = gambit_create_handler が行う。

この gambit_create_handler は以下処理を行う。

static handler *gambit_create_handler(handlerton *hton, TABLE_SHARE *table,
                                       bool, MEM_ROOT *mem_root) {
  return new (mem_root) ha_gambit(hton, table);
}

またここで呼ばれる ha_gambit は以下の通り。

ha_gambit::ha_gambit(handlerton *hton, TABLE_SHARE *table_arg)
    : handler(hton, table_arg) {}

以下のドキュメントとは微妙にこのハンドラの特に定義部分が異なるが、メソッドポインタなどの登録は初期化関数内で行った方が良さそう。
また、これはどういうことなのかまだわかっていないがストレージエンジンのメタ情報は以下の mysql_declare_plugin で定義するようになっているみたい。*1

mysql_declare_plugin(gambit){
    MYSQL_STORAGE_ENGINE_PLUGIN,
    &gambit_storage_engine,
    "GAMBIT",
    "lrf141",
    "Gambit simple storage engine",
    PLUGIN_LICENSE_GPL,
    gambit_init_func, /* Plugin Init */
    NULL,              /* Plugin check uninstall */
    NULL,              /* Plugin Deinit */
    0x0001 /* 0.1 */,
    func_status,              /* status variables */
    gambit_system_variables, /* system variables */
    NULL,                     /* config options */
    0,                        /* flags */
} mysql_declare_plugin_end;

この mysql_declare_plugin の実体はマクロ。
mysql-server/plugin.h at 4869291f7ee258e136ef03f5a50135fe7329ffb9 · mysql/mysql-server · GitHub
mysql-server/plugin.h at 4869291f7ee258e136ef03f5a50135fe7329ffb9 · mysql/mysql-server · GitHub



ドキュメントは以下を参照している。
dev.mysql.com

テーブルを作成する

ここまでで、前提となるストレージエンジンの大まかな解説は終わりにして、早速実装していく。
まずはテーブルを作るところから。
基本的には ha_gambit.cc の ha_gambit::create メソッドを実装していく形になる。
公式ドキュメントは以下のページ。
MySQL :: MySQL Internals Manual :: 23.8 Creating Tables

この関数はテーブルを作る際に呼ばれるとなっていて、基本的には my_create を呼び出すことでファイルを作成するようになっている。
ただ、その前にドキュメントには記載されていない create メソッドにあるこの存在。
まずはこれが何か読み解く。

int ha_gambit::create(const char *name. TABLE *, HA_CREATE_INFO *. dd::Table) {
...

  /*
    It's just an gambit of THDVAR_SET() usage below.
  */
  THD *thd = ha_thd();
  char *buf = (char *)my_malloc(PSI_NOT_INSTRUMENTED, SHOW_VAR_FUNC_BUFF_SIZE,
                                MYF(MY_FAE));
  snprintf(buf, SHOW_VAR_FUNC_BUFF_SIZE, "Last creation '%s'", name);
  THDVAR_SET(thd, last_create_thdvar, buf);
  my_free(buf);

  uint count = THDVAR(thd, create_count_thdvar) + 1;
  THDVAR_SET(thd, create_count_thdvar, &count);

  return 0;
}

MySQL: THD Class Reference
THD はクライアントの接続毎に、Thread/Connction ディスクリプタとして動作する個別のスレッドを作成するクラス。
このクラスがもつメソッド達をみても間違いないはず。

一度スレッドを取得したあとは、メモリを確保してバッファにログ(?)を書き込む。
そのあとはスレッドローカルの値を buf に更新する。
そして、スレッド数を1加算してからスレッドローカルに同様に保存している。

このスレッドに何の意味があるかわからなかったので InnoDB の ha_innobase::create を読んでみると trx とあったのでおそらくトランザクションを実装するときに使うのだろうと思った。ここはトランザクション関連をやるまでわからない。
それか、PSI とあるので性能計測系のスレッドかもしれない。
mysql-server/ha_innodb.cc at 91a17cedb1ee880fe7915fb14cfd74c04e8d6588 · mysql/mysql-server · GitHub

その前にいくつか前提を抑えておく。
この create メソッドでは基本的に my_craate 関数を使ってデータを保存するファイルを作るっぽいが、my_create が何を引数にして何を返すかがドキュメントにはないので以下の MySQL のコードとコードドキュメントを読む。
mysql-server/my_create.cc at 91a17cedb1ee880fe7915fb14cfd74c04e8d6588 · mysql/mysql-server · GitHub
MySQL: Mysys - low level utilities for MySQL

my_create の引数は FileName, CreateFlags, access_Flag, MyFlags とある。
FileName はそのままだが、問題は CreateFlags と access_Flag、 MyFlags。
CreateFlags は O_CREAT と OR が計算されるようになっている。これは open システムコールを呼ぶときにファイルが存在しない場合はそのファイルを作成することが前提となるため。なのでここでは、open システムコールに渡すのと同じものを渡せば確実にそれと同一の挙動を取る。
access_flag は O_CREAT が前提になっているため、どのような権限をそのファイルに付与するかどうかを決定付ける。

問題は MyFlags。
ここで special flag と書かれた myf という型は int のエイリアスとなっている。
MySQL: include/my_inttypes.h File Reference
で、結局これが何者なのかというとさっきの my_create のコードを見てみると明らかにエラーの場合に呼ばれている。
なにやらしらべてみると my_funcs に紐付いた値らしくなんらかの関数を呼ぶのに使用されているみたい。よくわからない。


この my_create では該当ファイルを開いたあと、ディレクトリ情報を同期する。*2
そのあと、ファイル情報を内部的に保持する。
また、ファイルを開く段階で該当ファイルが存在しない場合は作成されるようになっている。
mysql-server/my_create.cc at 91a17cedb1ee880fe7915fb14cfd74c04e8d6588 · mysql/mysql-server · GitHub

ここまで読んで気がついたけど、おそらく my_create は open システムコールのラッパーになっているっぽい。

これで実装していく。

int ha_gambit::create(const char *name, TABLE *, HA_CREATE_INFO *,
                       dd::Table *) {
  DBUG_TRACE;
  File create_file;
  DBUG_ENTER("ha_gambit::create");
  if ((create_file=my_create(name, 0, O_RDWR | O_TRUNC, MYF(0))) < 0)
      DBUG_RETURN(-1);
  if ((my_close(create_file, MYF(0))) < 0)
      DBUG_RETURN(-1);

  /*
    It's just an gambit of THDVAR_SET() usage below.
  */
  THD *thd = ha_thd();
  char *buf = (char *)my_malloc(PSI_NOT_INSTRUMENTED, SHOW_VAR_FUNC_BUFF_SIZE,
                                MYF(MY_FAE));
  snprintf(buf, SHOW_VAR_FUNC_BUFF_SIZE, "Last creation '%s'", name);
  THDVAR_SET(thd, last_create_thdvar, buf);
  my_free(buf);

  uint count = THDVAR(thd, create_count_thdvar) + 1;
  THDVAR_SET(thd, create_count_thdvar, &count);

  return 0;
}

全体としてはこんな感じ。
TABLE, HA_CREATE_INFO. dd::Table は今回使用してない。dd::Table に至っては Internal Manual には記載されていないのでまたコードを追う必要がある。
TABLE は frm に吐かれる情報をもっているとあるのでおそらく sdi に統合されている。 HA_CREATE_INFO もテーブルのメタデータを持っているが今回はとくにそれらをいじったりしないので使っていない。


ここまでやって、make, ストレージエンジンの適用をやったら試してみる。

mysql [sample]> create table test_table(id int, name varchar(255)) engine=gambit;
Query OK, 0 rows affected (0.19 sec)

テーブルをストレージエンジンを指定した状態で作成する。
そうして /var/lib/mysql 以下の sample という DB のディレクトリ以下に次のファイルが存在した。

$ ls
test_table  test_table_339.sdi

できたっぽい。
この sdi ファイルというのは ver 8.0.3 で追加されたものらしく、 serialized dictionary information というもの。
中身は json 形式になっていて、 従来の frm などメタデータを保持していたものの代替となっている。
これは一時的なテーブルスペースと UNDO テーブルスペースを除く全てのテーブルスペースに存在する。
MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.3 (2017-09-21, Release Candidate)

余談・気になったところ

これ CREATE TABLE 文使ったら即時、ファイルが生成されたけどチェックポイントとか関係なく動作しているのかどうなのか。

テーブルを開く

テーブルを作成することはできたが問題は、その次に待っているテーブルをオープンするところ。
これはドキュメントがめちゃくちゃ薄い。もっというなら、テーブルを開くことなのにファイルロックを調べてねとしか書いていない。
おそらくそれ以外は本当に特に決まっていなくて、ファイルロックさえ考慮すればどう実装してもいいと思われる。

それではまず、ここでやるべき事を考えていく。
テーブルを SELECT, INSERT などの時に開く処理を ha_gambit::open に実装するのがここでの一番大きな目標。

そのメソッドは次のように宣言されている。

int ha_gambit::open(const char *, int, uint, const dd::Table *)

MySQL Internal のドキュメントとは異なるが、 int はおそらく mode で O_RDONLY, O_RDWR を渡すことを想定していて、 uint はテーブルを開く前にロックをどのようにチェックするかが渡されるはず。

#define HA_OPEN_ABORT_IF_LOCKED   0   /* default */
 #define HA_OPEN_WAIT_IF_LOCKED    1
 #define HA_OPEN_IGNORE_IF_LOCKED  2
 #define HA_OPEN_TMP_TABLE         4   /* Table is a temp table */
 #define HA_OPEN_DELAY_KEY_WRITE   8   /* Don't update index */
 #define HA_OPEN_ABORT_IF_CRASHED  16
 #define HA_OPEN_FOR_REPAIR        32  /* open even if crashed */

MySQL :: MySQL Internals Manual :: 23.9 Opening a Table

そして、EXAMPLE エンジンをベースにしているため、以下の実装が追加されている。

int ha_gambit::open(const char *, int, uint, const dd::Table *) {
  DBUG_TRACE;

  if (!(share = get_share())) return 1;
  thr_lock_data_init(&share->lock, &lock, NULL);

  return 0;
}

get_share を使って、share lock info を取得している。これは get_share() と ha_gambit.h のメンバを見ればわかる。

share という Gambit_share 型のメンバはヘッダーファイルで宣言されていて、 get_share の振る舞いは ha_gambit.cc に存在する。

Gambit_share *ha_gambit::get_share() {
  Gambit_share *tmp_share;

  DBUG_TRACE;

  lock_shared_ha_data();
  if (!(tmp_share = static_cast<Gambit_share *>(get_ha_share_ptr()))) {
    tmp_share = new Gambit_share;
    if (!tmp_share) goto err;

    set_ha_share_ptr(static_cast<Handler_share *>(tmp_share));
  }
err:
  unlock_shared_ha_data();
  return tmp_share;
}

get_share ではまず get_ha_share_ptr で ha_share ポインタを初期化、取得している。
MySQL: handler Class Reference

この ha_share が何者なのかというと Handler_share ポインタを格納・取得するポインタみたい。
そのポインタを取得して Gambit_share 型としてキャストしている。この Gambit_share 型は ha_gambit.h で定義されている。

/** @brief
  Gambit_share is a class that will be shared among all open handlers.
  This gambit implements the minimum of what you will probably need.
*/
class Gambit_share : public Handler_share {
 public:
  THR_LOCK lock;
  Gambit_share();
  ~Gambit_share() { thr_lock_delete(&lock); }
};

でも結局この share とは何なのかという肝心なところがドキュメント*3に書いてない。
なので、ちょっと戻って MySQL Internal Manual の Overview を読んでみる。
MySQL :: MySQL Internals Manual :: 23.2 Overview

The MySQL server is built in a modular fashion:

The storage engines manage data storage and index management for MySQL. The MySQL server communicates with the storage engines through a defined API.

Each storage engine is a class with each instance of the class communicating with the MySQL server through a special handler interface.

Handlers are instanced on the basis of one handler for each thread that needs to work with a specific table. For example: If three connections all start working with the same table, three handler instances will need to be created.

Once a handler instance is created, the MySQL server issues commands to the handler to perform data storage and retrieval tasks such as opening a table, manipulating rows, and managing indexes.

Custom storage engines can be built in a progressive manner: Developers can start with a read-only storage engine and later add support for INSERT, UPDATE, and DELETE operations, and even later add support for indexing, transactions, and other advanced operations.

意訳)
MySQL はモジュールベースで構築されている。

...(中略)...

各ストレージエンジンはクラスであって、そのクラスは handler インターフェースを介してサーバと通信する。
ハンドラーは特定のテーブルを操作する必要があるスレッド毎に1つのハンドラーインスタンスを生成する。例:3つの接続が全て同じテーブルで動作する場合は 3 つのハンドラインスタンスが生成される。

...(中略)...

つまり各コネクション毎にハンドラインスタンスを生成し、それらのハンドラ間ではこの share を共有しているということだった。
また、 ha_share はパーティション化されていないハンドラ*4の場合は TABLE_SHARE::ha_share が呼ばれるらしくその情報を見ると合点がいった。

TABLE_SHARE というものは、テーブル毎に生成され前述の通りテーブル間で共有されるインスタンスでテーブルのメタデータを持っている。
そのため、テーブルファイルの読み込みにはこの share を取得するべきだったらしい。
ここでようやく get_share の話に戻ってくる。 get_share では、この share を取得し、存在する場合はそれを返し存在しない場合は新たに share を生成し該当するポインタに代入して返している。

次に、また話は戻り get_share の後に何が起こるか。
その次は以下の処理が行われる。

thr_lock_data_init(&share->lock, &lock, NULL);

この thr_lock_data_init のドキュメントは以下のリンクにある。
MySQL: include/thr_lock.h File Reference

そもそも、この thr_lock_data_init は thr_lock.h|.cc に実装されているものなのだが、この thr_lock では Posix thread *5を扱う場合の R/W ロックを提供するものが実装されている。thr_lock_data_init はその機能の一部。

MySQL の thr_lock では 2 種類のロックを持つ。
マスターロック(THR_LOCK)とロックインスタンス(THR_LOCK_DATA)があり、任意のスレッドは任意の数だけロックインスタンスを持つことができる。また当然の如く使い終わったら、解放する必要がある。

ここまで来るとなにをやっていたかが分かってきて、 ha_gambit::open メソッドではそれが呼ばれる度に THR_LOCK_DATA インスタンスを生成する処理がなされるということになる。

既存の実装は理解したので、ようやくファイルを開く処理を書く。
まず、ロックなどなどは置いといてまずファイルディスクリプタをどこかで保持する必要がある。ファイルを開いてファイルディスクリプタを取るには my_open を使用する。おそらく my_open もシステムコールの open のラッパーだと思う。
実装は以下の通り。

int ha_gambit::open(const char *name, int, uint, const dd::Table *) {
  DBUG_TRACE;

  File open_file;

  if (!(share = get_share())) return 1;
  thr_lock_data_init(&share->lock, &lock, NULL);
  
  if (!(open_file = my_open(name, O_RDWR, MYF(0))))
      return 1;
  share->table_file = open_file;
  return 0;
}
class Gambit_share : public Handler_share {
 public:
  THR_LOCK lock;
  File table_file;
  Gambit_share();
  ~Gambit_share() { thr_lock_delete(&lock); }
};

テーブル毎に share がひとつ存在するので、ファイルディスクリプタを share で持つようにしている。
スレッドのロックを扱うには thr_multi_lock を使う必要があるのだが、ここではまだ使っていない。

INSERT の実装

ここで本来は SELECT がドキュメントの順番では来るが SELECT するためのデータが無いので先に INSERT を実装する。

INSERTを実装するには ha_gambit::write_row を実装する。
ただここは、本当に実装の例が無いので以下の2つの記事を参考にする。

MySQL - 自作ストレージエンジンで初音ミクさんに歌っていただきましょう - こんぶのつけもの
MySQLのストレージエンジンを自作してみる - 備忘録の裏のチラシ

今回はまず先に実装を載せる。そのあとでなぜそれがそのようなコードになったか書いていく。
というのも、ドキュメントからでは何をしたらよいのかわからなかったため。

まずha_gambit.h に以下のを追加した。

#include "sql_string.h"

class Gambit_share : public Handler_share {
  ...
  const char *name
  File write_file;
  ...
};

class ha_gambit : public handler {
...
  String buffer;
public:
...

まず、share にテーブル名を保持するメンバと書き込み用のディスクリプタを保持するメンバを追加した。
読み込み用のディスクリプタと同じにしてしまうと、ひとつのディスクリプタを R/W で共有することになってしまうのであまりやりたくない。

次に行を格納するためのバッファを handler に追加している。この型は string でなく String *6である。
このクラスは Java で言うところの String, StringBuffer がいい感じに合体したクラスに近い。
MySQL: String Class Reference

次に問題の ha_gambit.cc の実装。

int ha_gambit::write_row(uchar *) {
  ha_statistic_increment(&System_status_var::ha_write_count);

  share->write_file = my_open(share->name, O_RDWR | O_APPEND, MYF(0));

  char att_buf[1024];
  String attribute(att_buf, sizeof(att_buf), &my_charset_bin);
  my_bitmap_map *org_bitmap = tmp_use_all_columns(table, table->read_set);
  buffer.length(0);

  for (Field **field = table->field; *field; field++) {
    const char *p;
    const char *end;
    (*field)->val_str(&attribute, &attribute);
    p = attribute.ptr();
    end = attribute.length() + p;
    buffer.append('"');
    for (; p < end; p++)
        buffer.append(*p);
    buffer.append('"');
    buffer.append(',');
  }
  buffer.length(buffer.length() - 1);
  buffer.append('\n');
  tmp_restore_column_map(table->read_set, org_bitmap);
  int size = buffer.length();
  my_write(share->write_file, (uchar *)buffer.ptr(), size, MYF(0));
  return 0;
}

ha_statistic_increment とは、table が保持する THR 構造体のステータスを加算するもので今回は ha_write_count がインクリメントされている。
これはテーブルのステータス系表示に使われる。ので、最悪なくてもよさ気?
my_bitmap_map の tmp_use_all_columns は読み取りフラグを立てているがそのフラグが一体どこで利用されているかはわからない。

その次は、前述の書き込み用のファイルディスクリプタを取得するやつ。
attribute はこんな書き方があるのかと驚いている、これは C++ 独自の書き方なのかわからないがやっていることは char 型の配列を sql_string.h の String に直してかえしてくれるやつぐらいのイメージしかないが全くわからない。

そのあとはドキュメントに書いてあるように、 Field をループして、 String として値を取得したら 1 文字ずつ取り出して buffer に append している。
これをあとは最初に取得したファイルディスクリプタに登録するだけ。

ha_tina の存在

ha_tina という CSV ストレージエンジンが MySQL のコードにあるがこれを参考にして作られているものが多いのでこれをみるべきだったかもしれない。

テーブルスキャン

いよいよラスト、だがこれが一番面倒。
実装する関数が以下のように複数ある。

  • store_lock
  • external_lock
  • rnd_init
  • info
  • extra
  • rnd_next

実際に CSV ストレージエンジンで 9 行スキャンする場合はこう呼び出される。

ha_tina::store_lock
ha_tina::external_lock
ha_tina::info
ha_tina::rnd_init
ha_tina::extra - ENUM HA_EXTRA_CACHE   Cache record in HA_rrnd()
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::rnd_next
ha_tina::extra - ENUM HA_EXTRA_NO_CACHE   End caching of records (def)
ha_tina::external_lock
ha_tina::extra - ENUM HA_EXTRA_RESET   Reset database to after open

store_lock の実装

MySQL :: MySQL Internals Manual :: 23.10.1 Implementing the store_lock() Method
このメソッドは R/W の実行前に呼ばれる。行レベルロックやテーブルレベルロックなど内部ロックに関する実装をする。

ロックは今回実装しないので EXAMPLE からいじらない。

THR_LOCK_DATA **ha_gambit::store_lock(THD *, THR_LOCK_DATA **to,
                                       enum thr_lock_type lock_type) {
  if (lock_type != TL_IGNORE && lock.type == TL_UNLOCK) lock.type = lock_type;
  *to++ = &lock;
  return to;
}

やってることは、ハンドラのもつロックインスタンスがアンロック状態で ignore しないなら SELECT 時に共有ロック、 INSERT 系の場合は排他ロックをセットする。

external_lock の実装

これは外部ロックに関する実装。具体的にはデータファイルに対するファイルロックを実装する。
これも今回は実装しないのでそのまま。

int ha_gambit::external_lock(THD *, int) {
  DBUG_TRACE;
  return 0;
}

rnd_init の実装

テーブルスキャンでしようする変数などを初期化するメソッド。
stats.record とポジションに関する情報を初期化しておく。

class ha_gambit : public handler {
...
  off_t current_position;
...
int ha_gambit::rnd_init(bool) {
  current_position = 0;
  stats.records = 0;
  return 0;
}

info の実装

これは先に実装を載せる。

int ha_gambit::info(uint) {
  if (stats.records < 2)
    stats.records= 2;
  return 0;
}

レコード数が 2 未満の場合にオプティマイザの処理が変わるらしく、この場合はレコードが読み込まれない可能性があるため if 文を追加しないといけない。
これはソースコードのコメントの注意書きにあった。

extra の実装

これはストレージエンジンに対してヒントを送るための関数らしいので return 0 のみの状態で今回は放置する。

rnd_next の実装

これがファイルからレコードを読み込むメソッドとなる。書き込みの時とほぼ同じ処理をする。

int ha_gambit::rnd_next(uchar *buf) {
  DBUG_TRACE;
  int err;

  ha_statistic_increment(&System_status_var::ha_read_rnd_next_count);
  err = find_current_row(buf);
  if (!err) 
      stats.records++;
  return err;
}

int ha_gambit::find_current_row(uchar *buf) {
  DBUG_TRACE;

  my_bitmap_map *org_bitmap = tmp_use_all_columns(table, table->write_set);
  uchar read_buf[IO_SIZE];
  bool is_end;
  uchar *p;
  uchar current_char;
  uint bytes_read;

  memset(buf, 0, table->s->null_bytes);
  for (Field **field = table->field; *field; field++) {
    bytes_read = my_pread(share->table_file, read_buf, sizeof(read_buf), current_position, MYF(0));
    if (!bytes_read) {
      tmp_restore_column_map(table->write_set, org_bitmap);
      return HA_ERR_END_OF_FILE;
    }
    p = read_buf;
    current_char = *p;
    buffer.length(0);
    is_end = false;

    for (;;) {
      if (current_char == '"') {
        if (is_end) {
          current_position += 2;
          break;
        }
        is_end = true;
      } else {
        buffer.append(current_char);
      }
      current_char = *++p;
      current_position++;
    }
    (*field)->store(buffer.ptr(), buffer.length(), buffer.charset());
  }
  tmp_restore_column_map(table->write_set, org_bitmap);
  return 0; 
}

memset は今回は null を許容していないため、 先頭にあるカラム分の NULL ビットマップを 0 で埋めている。
この書き込み時の話は度々参考にしている以下のブログによくまとまっている。
MySQLのストレージエンジンを自作してみる - 備忘録の裏のチラシ

おわりに

INSERT, SELECT に関することは調べることが多すぎてめちゃくちゃ駆け足になってしまったのでまたこの手のブログを書くときに実装を追っていきたい。
次、やるときはそのあたりと index or トランザクション周りでもやろうかな
あと、C++ 今回はじめて書いたけどわからない文法が多すぎるからそっちもやらねば。

卒論執筆のいい気分転換になった

*1:ストレージエンジンに限らず、プラグインはこのような定義をソースコード末尾に持っている。

*2:Linux でのみこの動作が発生する

*3:ここで指すドキュメントとは MySQL 8.0.18 の Source Code DOcumentation のこと

*4:何を言っているかわからないがおそらくやっていない

*5:POSIX Thread はスレッドの POSIX 標準。スレッドの生成や操作などの API を定義している。

*6:sql_string.h の String クラス