🈷️

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プラグインっていうのは

https://sqlc.dev/

sqlcというのがsqlを入力にしてそのsqlをいい感じに実行できるソースコードが出力できるツールです。

いい感じっていうのは静的型付け言語ならsqlの実行に必要なパラメータ、実行して得られる結果が型で指定されている状態だったり。

https://docs.sqlc.dev/en/latest/guides/plugins.html

sqlcのプラグインがいい感じに出力するためのロジックそのものです。
プラグインの入出力はprotobufで定義されていてどんなプログラム言語でも標準入力、標準出力でやりとりすることができます。
標準入力、標準出力でやりとりするのにシンプルなコマンドで実行して起動するプロセスタイプのものか、wasmで実行する2通りの方法があります。

MoonBitといえばwasmが出力できるのが魅力の一つなので今回チャレンジしてみようというわけです。

プラグインの入出力を定義するprotobufはsqlcリポジトリ内になります。
サブモジュールとして追加しておくと便利です。
https://github.com/sqlc-dev/sqlc/blob/main/protos/plugin/codegen.proto

$ git submodule add git@github.com:sqlc-dev/sqlc.git sqlc

MoonBit with protoc

https://protobuf.dev/

Protocol Buffersがなんぞやというのは割愛します。

protoファイルで定義されたスキーマからprotobuf形式のメッセージを様々な言語で扱うためのツールが世に出ています。

なんとMoonBitではすでにprotobufメッセージを扱うツールが存在しています。
https://github.com/moonbitlang/protoc-gen-mbt
ですが、今回使いたいprotoファイルである「codegen.proto」はprotoc-gen-mbtに存在する不具合を踏むことになります。

https://github.com/moonbitlang/protoc-gen-mbt/pull/72

protoファイル側を書き換えてしまえば問題ないので書き換えてしまいます。

$ mkdir proto
$ cp -r sqlc/protos/plugin/codegen.proto proto/codegen.proto

paramsフィールドの名前をjson_nameに合わせてparametersに変更します。

https://github.com/sqlc-dev/sqlc/blob/main/protos/plugin/codegen.proto#L105-L114

https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/proto/codegen.proto#L105-L114

protoc-gen-mbtはprotocコマンドのプラグインとして実装されています。なのでprotocコマンドがあれば十分ですが、個人的にprotocコマンドよりbufを使っているのでbufを使っていくことにします。

https://buf.build/product/cli

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の実行ファイルを指定します。

buf.yaml
version: v2
modules:
  - path: proto

buf.gen.yaml
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のプラグインは異常終了した時に標準エラー出力したモノをログに出します。
逆に正常終了した場合には標準エラー出力したモノはログにでないので注意が必要です。

dump_stdin_to_stderr.sh
#!/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
sqlite/schema.sql
CREATE TABLE authors (
          id   integer    PRIMARY KEY AUTOINCREMENT,
          name text   NOT NULL,
          bio  text
);
sqlite/query.sql
/* 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の構成ファイルを作ります。

sqlc.yaml
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を編集して参照します。

https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/moon.mod.json#L4-L8

実行時のエントリーポイントになるパッケージのmoon.pkg.json

cmd/main/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を呼び出してプロセスを異常終了させています。

cmd/main/main.mbt
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で指定します。

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