私は、 ruby-duckdb という gem を開発しています。普段、 gem を開発する上で考えていることを書いてみたいと思います。
複数のRubyのバージョンで動作する
普段、業務で開発している Rails アプリケーションとは違い、 gem を使うユーザーは、 どのバージョンの Ruby を使うかわかりません。
なので、広く使われるように、複数の Ruby で使えるようにしたいと考えてます。
とはいえ、古いバージョンのサポートを続けるのは、労力がかかるので、 Ruby が公式にメンテしているバージョンに限定しています。
手元で全バージョンを確認するのも手間なので、CI (GitHub Actions) を使って自動テストを実行して動作確認しています。
なお、CIで実行するときは、head や preview バージョン (2023年11月26日時点では、 3.3.0-preview2)でもテストしています。
私は、開発するときは、最新のバージョンを使っています。
たまに、Ruby 3.2 では、テストが通って、 Ruby 2.7 ではテストが通らないことがあり、その都度、どちらでも動作する書き方に変更しています。
例えば、 Ruby 3.2 では、
def foo(*args, **kwargs)
...
end
def bar(*, **)
foo(*, **)
end
と書くことができますが、 Ruby 2.7 では SyntaxError
になるため、
def foo(*args, **kwargs)
...
end
def bar(*args, **kwargs)
foo(*args, **kwargs)
end
と修正する必要がありました。
場合によっては、 RUBY_VERSION
でコードを分岐させる必要がありますが、コードが複雑になってしまうので、極力、そういった書き方は避けるようにしてます。
gem ユーザーの立場では、なんでこのバージョンのRubyで動作しないのとか、思ったりすることもありますが、
gem 作者の立場では、複数バージョンをサポートするのは面倒なので、オフィシャルでサポートされているバージョンに限定しています。
複数のバージョンの duckdb で動作する
duckdb は、LTS なバージョンは存在しないと思ってます。
ですが、将来、LTS なバージョンが出てくることが予想されます。
その時、慌てることがないように、複数バージョンの duckdb でも動作するようにしようと意識して開発してます。
2023年11月26日現在では、最新バージョン (0.9.x) と1つ前のバージョン (0.8.x) のどちらでも動作するようにしています。
ただし、最新版でのみ使える機能の場合、 最新版の duckdb と組み合わせたときは使えるが、1つ前のバージョンと組合せたときは、その機能だけが使えないといったことがあります。
例えば、
- duckdb 0.9.2 と組み合わせた場合に
DuckDB::Connection#interrupt
というメソッドが使える。 - duckdb 0.8.1 と組み合わせた場合に
DuckDB::Connection#interrupt
というメソッドは定義されない。
といった違いが発生しますが、どちらの duckdb のバージョンでも使える機能は同じように使えるように開発してます。
複数のOSで動作する
ruby-duckdb は、 duckdb に依存しているのですが、 duckdb が動作する OS では、 ruby-duckdb も動作するのが理想だと考えています。
ですので、 GitHub Actions でサポートされているOSでテストするようにしています。
例えば、 Windows で CI が失敗することがあったので、 duckdb
にPRを取り込んでもらったこともあります。
ちなみに私は、 Mac と Linux で duckdb を動かしたことがあるのですが、 Windows では一度も動かしたことがありません。
C 少な目、Ruby 多め
ruby-duckdb は C言語で書かれている部分と、Ruby で書かれている部分があるのですが、極力 C言語を少くして、Rubyで書ける部分を多くするようにしてます。
Ruby で書いた方がメンテナンスしやすいためです。
あと、 Rubyユーザーなら、 Ruby で書かれたコードが多い方がとっつきやすく、 PR とかも出してもらいやすいんじゃないかと考えているからです。
C言語の部分は duckdb の薄いラッパーにして極力単純に duckdb の C API を呼び出すだけにしています。
C言語部分では、duckdb の C API の関数を元にメソッドを定義してます。
Ruby のコードでは、ruby-duckdb ユーザーが使いやすいメソッドを定義して、メソッド内部でラッパーを呼び出すように実装しています。
最近では、Copilot が推測しやすいようなメソッド名やメソッドの別名を定義したりすることもあります。
例えば、薄いラッパーを使うと
prepared_statement = DuckDB::PreparedStatement.new(
db_connection,
'SELECT * FROM users WHERE name = $name'
)
i = prepared_statement.bind_parameter_index('name')
prepared_statement.bind_varchar(i, 'Bob')
result = prepared_statement.execute
となりますが、 Ruby 側で DuckDB::Connection#query
を定義していて
result = db_connection.query(
'SELECT * FROM users WHERE name = $name',
name: 'Bob',
)
と簡単に書くこともできるようになってます。
まとめ
gem 作者としては、より多くの場面で、より多くの人に gem を使ってもらえるように、それなりに色々と考えているんだということがわかっていただけると幸いです。