🌕

MoonBitで作るsqlc plugin。traitって楽しいよね!

に公開

この記事は https://qiita.com/advent-calendar/2025/moonbit の14日目です。


MoonBitでsqlc plugin作るシリーズ続編です。

https://zenn.dev/4245ryomt/articles/9680434dc60c0c
https://zenn.dev/4245ryomt/articles/4ab08cb8fbcc75

wasm触れてみるその2ではsqlc本体からpluginとしての生成リクエストを受け取り、生成レスポンスで応答するところまで実装して、実行をwasmで行うらへんまでやってみました。

fn process_generate_request(
  generate_request : @plugin.GenerateRequest,
) -> @plugin.GenerateResponse raise {
  let codes = "I am a file. contents wrote by MoonBit"
  let file = @plugin.File::new(
    "generated_queries.mbt",
    @encoding/utf8.encode(codes),
  )
  @plugin.GenerateResponse::new([file])
}

実際に動いてsqliteとやりとりをできる仕上がりになったので紹介と仕上げる過程で面白いなーと思ったところを書いてみました。

作ってみたコードをざっくり

作ってみたpluginのコードです。
https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/try_moonbit_sqlc_plugin_dev.mbt

下のようなクエリをsqlcに渡すと、

query.sql
/* name: list_authors :many */
SELECT * FROM authors
ORDER BY name;

/* name: create_author :exec */
INSERT INTO authors (
  id, name, bio
) VALUES (
  ?, ?, ? 
);

よしなにパラメータをとって返り値を構造化して扱えるようになります。

///|
fn main {
  let db = match @sqlite.Database::open(":memory:") {
    Some(d) => d
    None => {
      println("Failed to open database")
      return
    }
  }
  println("Database opened successfully")
  try {
    @lib.create_author_table(db) |> ignore
    @lib.create_author(db, 1, "John Doe", None) |> ignore
    @lib.create_author(db, 12, "D Doe", Some("hou")) |> ignore
    @lib.list_authors(db)
    .iter()
    .each(author => println(
      "Author ID: \{author.id}, Name: \{author.name}, Bio: \{author.bio}",
    ))
  } catch {
    err => {
      abort("Error: \{err}")
    }
  }
}

sqliteのやりとりでは以下のライブラリを利用させてもらっています。
https://mooncakes.io/docs/mizchi/sqlite

pluginでは以下のようなコードを生成していてsql実行の入出力を制御しています。

///|
pub(all) struct ResultOflist_authors {
  id : Int
  name : String
  bio : String?
} derive(ToJson, FromJson, Show)

///|
impl FromRow for ResultOflist_authors with from_row(row : @sqlite.Statement) -> ResultOflist_authors raise SQLiteError {
  try {
    let row = SqliteStatement(row)
    let id = row |> SqliteStatement::column(0)
    let name = row |> SqliteStatement::column(1)
    let bio = row |> SqliteStatement::column(2)
    ResultOflist_authors::{ id, name, bio }
  } catch {
    e => raise SQLiteError::RowConversionError(e)
  }
}

///|
pub fn list_authors(
  database : @sqlite.Database,
) -> Array[ResultOflist_authors] raise SQLiteError {
  let sql =
    #|SELECT id, name, bio FROM authors
    #|ORDER BY name
  database
  .query(sql)
  .map(stmt => {
    let result = Array::new()
    for i in stmt.iter() {
      let row = FromRow::from_row(i)
      result.push(row)
    }
    stmt.finalize() |> ignore
    result
  })
  .unwrap_or_error(PrepareStatementError)
}

コード生成についてはsqlcから提供される構造化されたQueryを元にお手製のmustacheでMoonBitのコードを書き出しています。
https://mooncakes.io/docs/ryota0624/mustache

https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/try_moonbit_sqlc_plugin_dev.mbt#L367-L415

traitはコード生成に優しいかもしれない

ここまで書いてみておもしろーと思ったポイントを紹介

振る舞いは型から決まってくれる

以下のコードはResultOflist_authorsという構造体をsql実行結果から取得した行から返しています。
ResultOflist_authors型の宣言は各所にあり、関数のボディには返り値として生成している箇所があります。
一方でResultOflist_authorsの要求している各フィールドの型の宣言は出てきていません。
idはInt, name, bioはStringですが関数にそのことはコードとして現れていません。

impl FromRow for ResultOflist_authors with from_row(row : @sqlite.Statement) -> ResultOflist_authors raise SQLiteError {
  try {
    let row = SqliteStatement(row)
    let id = row |> SqliteStatement::column(0)
    let name = row |> SqliteStatement::column(1)
    let bio = row |> SqliteStatement::column(2)
    ResultOflist_authors::{ id, name, bio }
  } catch {
    e => raise SQLiteError::RowConversionError(e)
  }
}

let id = row |> SqliteStatement::column(0)の宣言中では以下のコードが関連してきます。
SqliteStatement::columnがいい感じにidが要求する型に合わせてくれます。
idはその後ResultOflist_authorsの初期化に使われるので、Int型であることが伝わり、SqliteStatement::columnIntを返すように振る舞います。

///|
priv struct SqliteStatement(@sqlite.Statement)

///|
fn[T : FromSqlValue] SqliteStatement::column(
  self : SqliteStatement,
  index : Int,
) -> T raise ColumnExtractionError {
  from_sql_value(self.0.column(index), index)
}

///|
priv trait FromSqlValue {
  from_sql_value(value : @sqlite.SqlValue, columnIndex : Int) -> Self raise ColumnExtractionError
}

traitによって振る舞いを切り替えているのでInt型に対応したtraitの実装があります。

impl FromSqlValue for Int with from_sql_value(
  value : @sqlite.SqlValue,
  columnIndex : Int,
) -> Int raise ColumnExtractionError {
  match value {
    @sqlite.SqlValue::Int(v) => v
    @sqlite.SqlValue::Int64(v) => v.to_int()
    _ =>
      raise ColumnExtractionError(
        (columnIndex, "type mismatch actual \{sqlValueTypeName(value)}"),
      )
  }
}

pluginから出力するコードからはsqliteとやりとりしうる型分のtraitの実装があります。
https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/generated/sqlite_utility.mbt#L97-L182

見方を変えるとBool型をsqliteで直接扱わないのでFromSqlValueの実装を用意していないので、Bool型を要求しながらSqliteStatement::columnを呼び出してもエラーになります。

入出力の型を埋めるて振る舞いを決める

traitを活用して内側の振る舞いが決まってくれるようになると、コード生成で出力するコードが減らしやすい...と思いました。

明示的に型を合わせるための振る舞いコードを出力する必要があると以下のようになるなと考えていますがその必要はないです。

let row = SqliteStatement(row)
let id = row |> SqliteStatement::column_int(0)
let name = row |> SqliteStatement::column_text(1)
let bio = row |> SqliteStatement::column_text_option(2)
ResultOflist_authors::{ id, name, bio }

自分が普段書いているDartやTypeScriptではできないできない(と思っている)動かし方なので楽しい。
この手の道具はアプリケーションで使おうと頑張りすぎると苦しい結果を招きうるが、ライブラリやフレームワーク作りにおいては楽しい。

Discussion