MoonBitで作るsqlc plugin。moonbit protocを使ったり、wasmに触れてみたり。その1
この記事は https://qiita.com/advent-calendar/2025/moonbit の10日目です。
この記事では
MoonBitでsqlcのプラグインを作ってみたことを紹介します。
この記事ではMoonBitでいい感じにSQLが実行できるライブラリは完成しません。
過程でMoonBitでprotocを使ったり、MoonBitでビルドしたwasmを実行しています。
書き始めたら思いの外長いドキュメントになってしまったので分割します。
この記事はその1でsqlcのプラグインとしての入力をMoonBitで解釈するまでしてみます。
その2でMoonBitで出力したwasmをsqlcのプラグインとして使ってみます。
この記事はその1です。
使っていくmoonコマンドは以下のバージョンです。
$ moon version ✘ 2
> moon 0.1.20251202 (1a59800 2025-12-02)
手始めにmoon newコマンドでプロジェクトを作ります。
$ moon new try_moonbit_sqlc_plugin_dev
$ cd try_moonbit_sqlc_plugin_dev
sqlcプラグインっていうのは
sqlcというのがsqlを入力にしてそのsqlをいい感じに実行できるソースコードが出力できるツールです。
いい感じっていうのは静的型付け言語ならsqlの実行に必要なパラメータ、実行して得られる結果が型で指定されている状態だったり。
sqlcのプラグインがいい感じに出力するためのロジックそのものです。
プラグインの入出力はprotobufで定義されていてどんなプログラム言語でも標準入力、標準出力でやりとりすることができます。
標準入力、標準出力でやりとりするのにシンプルなコマンドで実行して起動するプロセスタイプのものか、wasmで実行する2通りの方法があります。
MoonBitといえばwasmが出力できるのが魅力の一つなので今回チャレンジしてみようというわけです。
プラグインの入出力を定義するprotobufはsqlcリポジトリ内になります。
サブモジュールとして追加しておくと便利です。
$ git submodule add git@github.com:sqlc-dev/sqlc.git sqlc
MoonBit with protoc
Protocol Buffersがなんぞやというのは割愛します。
protoファイルで定義されたスキーマからprotobuf形式のメッセージを様々な言語で扱うためのツールが世に出ています。
なんとMoonBitではすでにprotobufメッセージを扱うツールが存在しています。 ですが、今回使いたいprotoファイルである「codegen.proto」はprotoc-gen-mbtに存在する不具合を踏むことになります。
protoファイル側を書き換えてしまえば問題ないので書き換えてしまいます。
$ mkdir proto
$ cp -r sqlc/protos/plugin/codegen.proto proto/codegen.proto
paramsフィールドの名前をjson_nameに合わせてparametersに変更します。
protoc-gen-mbtはprotocコマンドのプラグインとして実装されています。なのでprotocコマンドがあれば十分ですが、個人的にprotocコマンドよりbufを使っているのでbufを使っていくことにします。
protoc-gen-mbtをbufから呼び出す
まずは protoc-gen-mbtをソースコードからビルドします。
$ git clone git@github.com:moonbitlang/protoc-gen-mbt.git tmp
$ cd tmp
$ moon build -C cli
$ cp cli/target/native/release/build/protoc-gen-mbt.exe ../protoc-gen-mbt.exe
$ cd ..
$ rm -rf tmp
bufの構成ファイル2つではprotoファイルの置き場とビルドしたprotoc-gen-mbtの実行ファイルを指定します。
version: v2
modules:
- path: proto
version: v2
inputs:
- directory: proto
plugins:
- local: ["./protoc-gen-mbt.exe"]
out: .
opt:
# https://github.com/moonbitlang/protoc-gen-mbt?tab=readme-ov-file#arguments
- project_name=sqlc_plugin
- json=false
- async=false
- username=yourname
buf generateコマンドでprotoファイルを元にMoonBitのソースコードを生成します。
$ buf generate
MoonBitのプロジェクトとしてソースコードは出力されます。
$ tree sqlc_plugin
> sqlc_plugin
> ├── moon.mod.json
> └── src
> └── plugin
> ├── moon.pkg.json
> └── top.mbt
sqlcプラグインの入力を見てみる
sqlcプラグインを作っていく上でまずは入力がどんな感じかを見ていきます。
というわけで標準入力をそのまま標準エラー出力に流してみます。
まず適当にshellの実行ファイルを作ります。
sqlcのプラグインは異常終了した時に標準エラー出力したモノをログに出します。
逆に正常終了した場合には標準エラー出力したモノはログにでないので注意が必要です。
#!/bin/sh
cat - >&2
exit 1
$ chmod +x dump_stdin_to_stderr.sh
sqlcでのコード生成をするために適当なsqlファイルを作ります。
$ mkdir sqlite
$ touch sqlite/schema.sql
$ touch sqlite/query.sql
CREATE TABLE authors (
id integer PRIMARY KEY AUTOINCREMENT,
name text NOT NULL,
bio text
);
/* name: get_author :one */
SELECT * FROM authors
WHERE id = ? LIMIT 1;
/* name: list_authors :many */
SELECT * FROM authors
ORDER BY name;
/* name: create_author :execresult */
INSERT INTO authors (
name, bio
) VALUES (
?, ?
);
/* name: delete_author :exec */
DELETE FROM authors
WHERE id = ?;
sqlファイルとplugin(shell実行ファイル)を指定してsqlcの構成ファイルを作ります。
version: '2'
plugins:
- name: dump_stdin_to_stderr
process:
cmd: ./dump_stdin_to_stderr.sh
sql:
- name: sqlite
schema: sqlite/schema.sql
queries: sqlite/query.sql
engine: sqlite
database:
uri: file:authors?mode=memory&cache=shared
codegen:
- out: generated
plugin: dump_stdin_to_stderr
sqlc generateでコード生成を実行するとdump_stdin_to_stderr.shで出力している入力がそのまま出力されます。
$ sqlc generate main ✭ ✱
> # package dump_stdin_to_stderr
> error generating code: process: error running command
> p
> 2sqlitesqlite/schema.sql"sqlite/query.sqlb>
> generateddump_stdin_to_stderr*
> ump_stdin_to_stderr.sh�main"�main�
> authors'
> id0���������R authorsb integer&
> name0���������R authorsbtext#
> bio0���������R authorsbtext�
そのまま見ても意味がわかりませんが、これでprotobuf形式のバイナリが流れてきているのが確認できています。
sqlc pluginの入力をMoonBitで扱う
ようやくここにきてMoonBitのコードを書いていきます。
まずは必要なモジュールをプロジェクトへの追加をします。
$ moon add moonbitlang/async
$ moon add moonbitlang/protobuf
bufで出力したsqlcプラグインの入出力を定義したモジュールは直接moon.mod.jsonを編集して参照します。
実行時のエントリーポイントになるパッケージのmoon.pkg.json
{
"is-main": true,
"import": [
{
"path": "yourname/try_moonbit_sqlc_plugin_dev",
"alias": "lib"
},
"moonbitlang/async/stdio",
"moonbitlang/async",
"moonbitlang/async/io",
"yourname/sqlc_plugin/plugin",
"moonbitlang/protobuf"
]
}
標準エラーにGenerateRequestメッセージに含まれるクエリの名称が含まれるのでそれを流します。
最後にpanicを呼び出してプロセスを異常終了させています。
fn main {
@async.run_async_main(main_async)
panic()
}
async fn main_async() -> Unit {
let input = @stdio.stdin.read_all()
let request = @lib.parse_generate_request(input.binary())
for query in request.queries {
@stdio.stderr.write(query.name + "\n")
}
}
fn parse_generate_request(data : Bytes) -> @plugin.GenerateRequest raise {
@protobuf.BytesReader::from_bytes(data) |> @protobuf.Read::read
}
moon buildコマンドで出力をnativeにして実行するとtarget/native/release/build/cmd/main/main.exeに実行ファイルができています。
$ moon build --target native
出力した実行ファイルをsqlc.yamlで指定します。
version: '2'
plugins:
- name: moonbit
process:
cmd: target/native/release/build/cmd/main/main.exe
sql:
- name: sqlite
schema: sqlite/schema.sql
queries: sqlite/query.sql
engine: sqlite
database:
uri: file:authors?mode=memory&cache=shared
codegen:
- out: generated
plugin: moonbit
sqlc generateを実行すればクエリの名称が出力されます。
& sqlc generate
> # package moonbit
> error generating code: process: error running command get_author
> list_authors
> create_author
> delete_author
その2へ続く!
Discussion