この記事は RubyでDBMS Advent Calendar 2019 の1日目の記事です。
全体の概要
ブラックボックスなデータベースの内部実装を少しでも理解したいと思った筆者が、
25日かけてオレオレDBMSをスクラッチで実装していくAdvent Calendar 2019の企画となります。
言語はRuby使います。
このようなミドルウェアを書くとなると、最近だとパフォーマンスや並行処理の書きやすさの観点からGoやRustなどが多いのかもしれませんが、
- 第一の目的はDBMSの内部実装を理解することであり、パフォーマンスは特に考慮しない
- 筆者の現在の業務でのメイン言語がRuby(余計な部分で詰まらないようにしたい)
- あまり例がないと思うので、弊社内の人間を始めとして多くのRubyistに読んでもらえたら嬉しい
などの理由でRubyで書いていくことにしました。
(Rust勉強したいのでいつかRustで書き直したりもしたい)
参考資料
CMU(カーネギーメロン大学)のDATABASE SYSTEMSという講義をメインに参考にしていきます。
非常に質の高い講義のスライド、ノート、動画がすべて無料で見れるためオススメです。
また、今年話題のO'Reillyのデータ指向アプリケーションデザインも適宜参考にするかもしれません。
成果物
こちらのGitHubリポジトリにday01
のように日付でディレクトリを切ってその日実装したコードを上げていく予定です。
本日の概要
初回なので、
サーバコマンド(MySQLのmysqldに相当)と、
クライアントコマンド(MySQLのmysqlに相当)を用意します。
また、RubyのRDBといいうことでRBDBと名付けました。
サーバコマンド
サーバはHTTPサーバとして立てます。
/exec?query=hoge
というパスにクエリパラメータとしてクエリを渡すことで実行される仕様です。
Rubyの標準ライブラリに含まれるWEBrickを使ってチャチャっと作っていきます。
./rbdbd
と実行ファイルを実行することで、以下のServerクラスのstartメソッドが呼ばれるようになっています。
(詳しくはソースを参照)
ひとまずqueryに渡された値をechoするだけにしておきます。
# frozen_string_literal: true
require 'webrick'
module Rbdb
class Server
def initialize(host: 'localhost', port: 40219)
@host = host
@port = port
end
def start
server.mount_proc '/exec' do |req, res|
query = req.query['query']
# TODO:
res.body = query
end
trap('INT') do |_|
terminate
end
server.start
terminate
rescue Interrupt
terminate
end
private
def terminate
server.shutdown
puts "\ngood bye!"
end
def server
@server ||= WEBrick::HTTPServer.new({
DocumentRoot: '/',
BindAddress: @host,
Port: @port,
})
end
end
end
実行してみます。
% ./bin/rbdbd
[2019-12-01 22:07:43] INFO WEBrick 1.4.2
[2019-12-01 22:07:43] INFO ruby 2.6.5 (2019-10-01) [x86_64-darwin18]
[2019-12-01 22:07:43] INFO WEBrick::HTTPServer#start: pid=28968 port=40219
% curl "http://localhost:40219/exec?query=hoge"
hoge%
クライアントコマンド
次にクライアント側です。
./rbdb
と実行すると、プロンプトを表示して入力を待ちます。
(遊び心で最初にアスキーアートを表示してみました)
exit
, quit
、あるいはCtrl+cで終了します。
こちらもひとまず、サーバからのレスポンスボディをそのまま表示するだけです。
# frozen_string_literal: true
require 'cgi'
require 'net/http'
module Rbdb
class Client
def initialize(host: 'localhost', port: 40219)
@host = host
@port = port
end
def start
show_title
show_prompt
while query = gets.chomp do
break if terminate_query?(query)
res = exec_query(query)
# TODO:
puts res
show_prompt
end
terminate
rescue Interrupt
terminate
end
private
def show_title
puts <<~'EOS'
_ _ _
_ __| |__ __| | |__
| '__| '_ \ / _` | '_ \
| | | |_) | (_| | |_) |
|_| |_.__/ \__,_|_.__/
EOS
end
def show_prompt
print '>> '
end
def terminate_query?(query)
query == 'exit' || query == 'quit'
end
def exec_query(query)
Net::HTTP.get(@host, "/exec?query=#{CGI.escape(query)}", @port)
end
def terminate
puts "\ngood bye!"
end
end
end
実行してみます。
% ./bin/rbdb
_ _ _
_ __| |__ __| | |__
| '__| '_ \ / _` | '_ \
| | | |_) | (_| | |_) |
|_| |_.__/ \__,_|_.__/
>> SELECT * FROM users;
SELECT * FROM users;
>> ^C
good bye!
まとめ
初日ということで、特にDBに関係ない内容となってしまいましたが、
明日からはサーバが受け取ったクエリを解析していく部分から実装していく予定です。