# 海外开发者实践分享：用 MoonBit 开发 SQLC 插件（其三）

**URL:** <https://taolun.moonbitlang.com/t/topic/1377>\
**Category:** Moonbit（月兔平台）\
**Created:** [2025 年12 月 24 日 10:04 UTC](https://taolun.moonbitlang.com/t/topic/1377 "2025-12-24T10:04:44Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![moonbitlang](https://taolun.moonbitlang.com/user_avatar/taolun.moonbitlang.com/moonbitlang/32/78_2.png) [@moonbitlang](https://taolun.moonbitlang.com/u/moonbitlang)\
发布日期： [2025 年12 月 24 日 10:04 UTC](https://taolun.moonbitlang.com/t/topic/1377/1 "2025-12-24T10:04:44Z")

</div>

近日，日本开发者 **4245ryomt** 在 Zenn 上发布了一系列围绕 **MoonBit 与 WebAssembly 插件实践** 的技术文章，分享了他使用 MoonBit 编写 **sqlc Wasm 插件** 的完整尝试过程。

文章通过可运行代码，介绍了插件请求处理、文件生成以及 Wasm 执行等关键环节，展示了 MoonBit 在工具链与 Wasm 场景下的实际开发体验，适合作为相关方向的实践参考。

 ![image](https://taolun.moonbitlang.com/uploads/default/original/1X/c5cb9b79326df1f99dd577f5e5dcdfbdf513cae5.png)

📄 原文链接：[MoonBitで作るsqlc plugin。traitって楽しいよね！](https://zenn.dev/4245ryomt/articles/f0da9cdb6c73c5)

↓以下为系列第三篇的原文翻译

这是 **MoonBit 制作 sqlc 插件系列的续篇** 。

在「 **（其二）** 」中，实现了：

从 sqlc 本体接收作为插件的生成请求，并通过生成响应进行返回，

一直做到 **以**  **wasm**  **的形式执行插件** 为止。

```rust
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**  **进行交互的完成状态** ，因此这里介绍一下最终成果，并写一写在完善过程中觉得“挺有意思”的地方。

## 大致介绍一下做出来的代码

这是制作出来的插件代码。\[1\]

```rust
// https://github.com/sqlc-dev/plugin-sdk-go/blob/main/metadata/meta.go#L15-L26///|
priv enum Cmd {
  Exec
  ExecResult
  ExecRows
  ExecLastId
  Many
  One
  CopyFrom
  BatchExec
  BatchMany
  BatchOne
} derive(Show)

///|
fn Cmd::from_string(cmd_str : String) -> Cmd raise {
  match cmd_str {
    ":exec" => Cmd::Exec
    ":execresult" => Cmd::ExecResult
    ":execrows" => Cmd::ExecRows
    ":execlastid" => Cmd::ExecLastId
    ":many" => Cmd::Many
    ":one" => Cmd::One
    ":copyfrom" => Cmd::CopyFrom
    ":batchexec" => Cmd::BatchExec
    ":batchmany" => Cmd::BatchMany
    ":batchone" => Cmd::BatchOne
    other => fail("Unknown command: \{other}")
  }
}

///|/// parse GenerateRequest from Bytes
pub fn parse_generate_request(data : Bytes) -> @plugin.GenerateRequest raise {
  @protobuf.BytesReader::from_bytes(data) |> @protobuf.Read::read
}

///|/// parse GenerateRequest from Array[Byte]
pub fn parse_generate_request_from_array_bytes(
  byte_array : Array[Byte],
) -> @plugin.GenerateRequest raise {
  Bytes::from_array(byte_array) |> parse_generate_request
}

///|
pub fn process_generate_request(
  generate_request : @plugin.GenerateRequest,
) -> @plugin.GenerateResponse raise {
  let codes = generate_code(generate_request)
  let query_file = @plugin.File::new(
    "queries.mbt",
    @encoding/utf8.encode(codes),
  )
  let sqlite_pkg_json =
    #|{
    #| "import": [
    #| "mizchi/sqlite"
    #| ]
    #|}
  let pkg_json_file = @plugin.File::new(
    "moon.pkg.json",
    @encoding/utf8.encode(sqlite_pkg_json),
  )
  let utility_code_file = @plugin.File::new(
    "sqlite_utility.mbt",
    @encoding/utf8.encode(
      sqlite_utility_code_error +
      sqlite_utility_code_to_sql_value +
      sqlite_utility_code_from_sql_value,
    ),
  )
  @plugin.GenerateResponse::new([query_file, utility_code_file, pkg_json_file])
}

///|let sqlite_utility_code_error =
  #| ///|
  #|pub suberror SQLiteError {
  #| RowConversionError(ColumnExtractionError)
  #| PrepareStatementError
  #| QueryExecutionError
  #|} derive(Show)
  #|
  #| ///|
  #|pub suberror ColumnExtractionError (Int, String) derive(Show)
  #|
  #|///|
  #|fn sqlValueTypeName(value : @sqlite.SqlValue) -> String {
  #| match value {
  #| @sqlite.SqlValue::Null => "Null"
  #| @sqlite.SqlValue::Int(_) => "Int"
  #| @sqlite.SqlValue::Int64(_) => "Int64"
  #| @sqlite.SqlValue::Double(_) => "Double"
  #| @sqlite.SqlValue::Text(_) => "Text"
  #| @sqlite.SqlValue::Blob(_) => "Blob"
  #| }
  #|}
  #|fn execute_statement(stmt : @sqlite.Statement) -> Unit raise SQLiteError {
  #| let result = stmt.execute()
  #| stmt.finalize() |> ignore
  #| if !result {
  #| raise SQLiteError::QueryExecutionError
  #| }
  #|}

///|///|let sqlite_utility_code_to_sql_value =
  #| ///|
  #|priv trait FromRow {
  #| from_row(row : @sqlite.Statement) -> Self raise SQLiteError
  #|}
  #| 
  #| ///|
  #|priv trait ToSqlValue {
  #| to_sql_value(Self) -> @sqlite.SqlValue
  #| }
  #| 
  #| ///|
  #| fn to_sql_values(array : Array[&ToSqlValue]) -> Array[@sqlite.SqlValue] {
  #| array.map(v => v.to_sql_value())
  #| }
  #| 
  #| impl ToSqlValue for Int64 with to_sql_value(self : Int64) -> @sqlite.SqlValue {
  #| @sqlite.SqlValue::Int64(self)
  #| }
  #| ///|
  #| impl ToSqlValue for Int with to_sql_value(self : Int) -> @sqlite.SqlValue {
  #| @sqlite.SqlValue::Int(self)
  #| }
  #| ///|
  #| impl ToSqlValue for Double with to_sql_value(self : Double) -> @sqlite.SqlValue {
  #| @sqlite.SqlValue::Double(self)
  #| }
  #| 
  #| ///|
  #| impl ToSqlValue for String with to_sql_value(self : String) -> @sqlite.SqlValue {
  #| @sqlite.SqlValue::Text(@encoding/utf8.encode(self))
  #| }
  #| 
  #| ///|
  #| impl ToSqlValue for Bytes with to_sql_value(self : Bytes) -> @sqlite.SqlValue {
  #| @sqlite.SqlValue::Blob(self)
  #| }
  #| 
  #| ///|
  #| impl[T : ToSqlValue] ToSqlValue for T? with to_sql_value(self : T?) -> @sqlite.SqlValue {
  #| match self {
  #| Some(value) => value.to_sql_value()
  #| None => @sqlite.SqlValue::Null
  #| }
  #| }
  #|
  #|

///|///|let sqlite_utility_code_from_sql_value =
  #| 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
  #| }
  #| 
  #| ///|
  #| impl FromSqlValue for String with from_sql_value(
  #| value : @sqlite.SqlValue,
  #| columnIndex : Int,
  #| ) -> String raise ColumnExtractionError {
  #| match value {
  #| @sqlite.SqlValue::Text(bytes) => {
  #| let text = try? (bytes |> @encoding/utf8.decode)
  #| text
  #| .map_err(_ => ColumnExtractionError((columnIndex, "utf8 decode error")))
  #| .unwrap_or_error()
  #| }
  #| a =>
  #| raise ColumnExtractionError(
  #| (columnIndex, "type mismatch actual \{sqlValueTypeName(a)}"),
  #| )
  #| }
  #| }
  #| 
  #| ///|
  #| impl FromSqlValue for Int with from_sql_value(
  #| value : @sqlite.SqlValue,

```

将下面这样的查询传给 sqlc：

**query.sql**

```SQL
/* name: list_authors :many */
SELECT * FROM authors
ORDER BY name;

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

```

就可以 **自动处理参数，并以结构化的形式来处理返回值** 。

```rust
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 交互时，使用了这个库\[2\]。

插件会生成如下形式的代码，用来控制 SQL 执行的输入与输出。

```rust
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\[3\] 模板来输出 MoonBit 代码\[4\]。

Lines 367 to 415 in main

```rust
fn generate_code_from_query(query : Query) -> String raise {
  let result_type_name = "ResultOf" + query.name
  let result_struct = @mustache.parse(
    result_type_struct_template,
    Json::object({
      "struct_name": Json::string(result_type_name),
      "fields": query.columns.mapi((index, col) => Json::object({
        "field_name": Json::string(col.name),
        "field_type": Json::string(col.mbt_type),
        "field_extractor_name": Json::string(
          ["row", sqlite_field_extractor_name_from_mbt_type(index)].join("|>"),
        ),
      })),
      "is_columns_empty": Json::boolean(query.columns.is_empty()),
    }),
  )
  let query_template = match query.cmd {
    Cmd::Many => list_query_template
    Cmd::One => find_one_query_template
    Cmd::Exec => exec_query_template
    other => fail("Unsupported command for code generation: \{other}")
  }
  let sql = query.sql
    .split("\n")
    .map(line => "#|" + line.to_string())
    .join("\n")
  let template_param = {
    "function_name": Json::string(query.name),
    "args": query.parameters.map(param => Json::object({
      "name": Json::string(param.column.name),
      "type": Json::string(param.column.mbt_type),
    }))
    |> Json::array,
    "result_type": Json::string(result_type_name),
    "sql": Json::string(sql),
  }
  let bind_all = if query.parameters.is_empty() {
    ""
  } else {
    @mustache.parse(bind_all_code, Json::object(template_param))
  }
  let query_func = @mustache.parse(
    query_template,
    Json::object(
      template_param.merge({ "bind_all_code": Json::string(bind_all) }),
    ),
  )
  result_struct + query_func
}

```

## trait 很适合用于代码生成

写到这里，介绍几个让我觉得挺有意思的地方。

### 行为由类型决定

下面这段代码，是从 SQL 执行结果的行中，返回一个 `ResultOflist_authors` 结构体。

`ResultOflist_authors` 类型的定义在多处出现，在函数体中也有将其作为返回值进行构造的地方。

但是， **函数体中并没有出现对各字段类型的显式声明** 。

虽然 `id` 是 `Int`，`name` 和 `bio` 是 `String`，但这些信息并没有直接写在函数代码中。

```rust
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::column` 会以返回 `Int` 的方式运行。

```rust
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 实现。

```rust
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)}"),
      )
  }
}

```

从插件输出的代码中\[5\]，可以看到：

针对所有可能与 sqlite 交互的类型，都准备了对应的 trait 实现。

```rust
impl FromSqlValue for String with from_sql_value(
  value : @sqlite.SqlValue,
  columnIndex : Int,
) -> String raise ColumnExtractionError {
  match value {
    @sqlite.SqlValue::Text(bytes) => {
      let text = try? (bytes |> @encoding/utf8.decode)
      text
      .map_err(_ => ColumnExtractionError((columnIndex, "utf8 decode error")))
      .unwrap_or_error()
    }
    a =>
      raise ColumnExtractionError(
        (columnIndex, "type mismatch actual \{sqlValueTypeName(a)}"),
      )
  }
}

///|
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)}"),
      )
  }
}

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

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

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

///|
impl[T : FromSqlValue] FromSqlValue for T? with from_sql_value(
  value : @sqlite.SqlValue,
  columnIndex : Int,
) -> T? raise ColumnExtractionError {
  if value is @sqlite.SqlValue::Null {
    None
  } else {
    Some(FromSqlValue::from_sql_value(value, columnIndex))
  }
}

```

换个角度来看：

由于 sqlite 并不会直接处理 `Bool` 类型，因此没有为 `Bool` 准备 `FromSqlValue` 的实现。

所以，如果在调用 `SqliteStatement::column` 时要求返回 `Bool` 类型，就会报错。

### 通过填充输入输出的类型来决定行为

当通过 trait 让内部行为自动决定之后，

就会觉得 **代码生成时需要输出的代码量可以大幅减少** 。

如果必须显式生成用于“匹配类型”的行为代码，可能就得写成下面这样，

但现在并不需要。

```rust
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 里 **做不到（或者我认为做不到）的写法，** 所以觉得很有意思。

这类工具，如果 **在应用层里过度勉强使用** ，可能会带来痛苦的结果；

但在 **库或框架的构建** 中，用起来就非常有趣。

* * *

1. [try\_moonbit\_sqlc\_plugin/try\_moonbit\_sqlc\_plugin\_dev.mbt at main · ryota0624/try\_moonbit\_sqlc\_plugin · GitHub](https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/try_moonbit_sqlc_plugin_dev.mbt) 

2. [mizchi/sqlite - mooncakes.io](https://mooncakes.io/docs/mizchi/sqlite) 

3. [ryota0624/mustache - mooncakes.io](https://mooncakes.io/docs/ryota0624/mustache) 

4. [try\_moonbit\_sqlc\_plugin/try\_moonbit\_sqlc\_plugin\_dev.mbt at main · ryota0624/try\_moonbit\_sqlc\_plugin · GitHub](https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/try_moonbit_sqlc_plugin_dev.mbt#L367-L415) 

5. [try\_moonbit\_sqlc\_plugin/generated/sqlite\_utility.mbt at main · ryota0624/try\_moonbit\_sqlc\_plugin · GitHub](https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/generated/sqlite_utility.mbt#L97-L182)
