ひむ日記

本名は設楽です

令和七年冬、DBD::mysql の quote メソッドを読む

はじめに

本エントリは MySQL Advent Calendar 2025 の 9 日目の記事です。
昨日は @HrsUed さんの wait/io/table/sql/handlerの正体 でした。

本エントリのあらすじとしては、libmysqlclient を用いたデータベースドライバの実装・処理の流れを追うことで MySQL C API を用いたライブラリの開発・使用の勘所を掴みたいというモチベーションのもと、Perl の代表的な MySQL ドライバである DBD::mysql の実行フローを追いながら理解を深めるぞ、というものになります。

いくつかのメソッドの実行フローを眺めようと思っていたのですが、quote だけでほどほどのボリュームになってしまったので、他のメソッドの動作については別のエントリでまた書こうと思います。

DBD::mysql とは?

PerlMySQL データベースを操作するために使用されるデータベースドライバ。
DBI という Perl のデータベースインタフェースに準拠した実装を提供している。

登場人物紹介

Perl アプリケーション

アプリケーション開発者が開発する、MySQL を利用するアプリケーション。
DBD::mysql をデータベースドライバとして用いる。

mysql.pm

DBD::mysql の一部。
mysql.pm にはエントリポイントとなるメソッドが定義されている。
これらは DBI で規定されたメソッドの実装となっているものが多い。

mysql.xs

DBD::mysql の一部。
mysql.xs では、Perl の実装と C の実装とのインタフェースが定義されている。
ここより下の実装は Dynamic Loading によってメモリに読み込まれる (libmysqlclient は Dynamic Linker によって解決される)。

dbdimp.c

DBD::mysql の一部。
dbdimp.c には C 言語での実装が定義されている。
Perl ランタイム側から XS 経由で呼ばれる。

libmysqlclient

C 言語で記述されたクライアントアプリケーションが MySQL Server と通信するために使用される C ベースの API の実装。
MySQL :: MySQL 8.4 C API Developer Guide :: 2 MySQL C API Implementations

quote メソッドの実行フローを追ってみる

DBD::mysql (が準拠する DBI) には $dbh->quote($value, $data_type) というメソッドが定義されている。

これは $value に含まれる特殊な文字をエスケープするなどして、SQL 文中に用いるリテラル値として使えるようにするために使用される。
undef が与えられた場合は "NULL" が出力される。

また、$data_type が与えられた場合は、その情報を基に $value に対して施すクオートの処理内容を決定する。

== Example ==
$dbh->quote("' OR 1=1 --")

== Output ==
'\' OR 1=1 --'

処理は大まかに以下の図に示した流れで進む。

sequenceDiagram
  participant A as Perlアプリケーション
  participant B as mysql.pm
  participant C as mysql.xs
  participant D as dbdimp.c
  participant E as libmysqlclient

  A->>B: call
  activate B
  B->>C: dispatch
  deactivate B
  activate C
  C->>D: call
  deactivate C
  activate D
  D->>E: call
  deactivate D
  activate E
  E->>D: return
  deactivate E
  activate D
  D->>C: return
  deactivate D
  activate C
  C->>A: return
  deactivate C

ここからは、各ステップでどのような処理が行われているかを具体的に読み進めていく。

1. Perl アプリケーション

$dbh->quote() が呼び出される。

2. mysql.pm

mysql.pm に定義されている DBD::mysql::db パッケージには、DBI の要求する quote メソッドが定義されていない。
このとき、DBImysql.xs で定義されている C 言語実装の XSUB を動的に"ディスパッチ"する。

The DBI "dispatches" the method calls to the appropriate driver for actual execution. The DBI is also responsible for the dynamic loading of drivers, error checking and handling, providing default implementations for methods, and many other non-database specific duties.

Architecture of a DBI Application

3. mysql.xs

mysql.xs には quote という C 言語で書かれた関数が定義されており、DBI によって透過的に呼び出される*1
https://github.com/perl5-dbi/DBD-mysql/blob/aead7bede40dd335412efe35f41d35e3144d01dd/mysql.xs#L413-L429

quote は Perl の引数 (dbhstrtype) を受け取り、非同期クエリの実行中チェックなどをした後に dbdimp.c のヘルパー関数 dbd_db_quote() を呼び出し、処理を委譲する。

4. dbdimp.c

mysql.xs から呼び出された dbd_db_quote は引数 (dbhstrtype) を受け取り、以下の順に処理を行なっていく。
https://github.com/perl5-dbi/DBD-mysql/blob/aead7bede40dd335412efe35f41d35e3144d01dd/dbdimp.c#L4586-L4642

a. 渡された文字列 str の有効性を確認し、undef である場合は "NULL" を返す

b. 型情報 type が与えられている場合はその型に対応する sql_type_info_t 構造体を取得し、数値型などのクオートすべきでない型であることが判明した場合は Nullsv を返して処理を切り上げる (この Nullsv は文字列としての "NULL" とは異なるので注意)

c. エスケープ後の文字列を格納するための新しい Perl スカラ値を作成し、メモリを割り当てる

メモリは 元の文字列の長さ * 2 + 3 の長さで割り当てられる。
なぜこうなっているのかと言うと、全ての文字がバックスラッシュでエスケープされる場合に最長で元の長さの 2 倍になる可能性があり、さらに開始の引用符 (')、終了の引用符 (') および C 言語において文字列の終端を表すヌル文字 (\0) が追加されるため最長で 元の文字列の長さ * 2 + 3 になるためだと思われる。

d. libmysqlclient ライブラリの mysql_real_escape_string_quote() を元の文字列を引数として渡した上で呼び出す

5. libmysqlclient

与えられた文字列にエスケープ処理を施し、エンコード済み文字列の長さを返す。
MySQL :: MySQL 8.4 C API Developer Guide :: 5.4.61 mysql_real_escape_string_quote()

6. dbdimp.c (戻り)

libmysqlclient によってエスケープが実行された文字列に対し*2、開始と終了の引用符を書き込みヌル文字 (\0) を追加したものを返す。

7. mysql.xs (戻り)

dbdimp.c によってエスケープされた文字列を Perl ランタイムに渡す際に、Perl の参照カウントを一時的に維持するために、mortal な値としてフラグをつけて返す。

おわりに

Perl ランタイムからぬるっと動的に C の実装が呼び出されている部分はすごく面白かったです。
また、メモリの割り当てや Perl ランタイム側に値を返す事情を考慮した設計は、Perl 以外の言語・フレームワークによるネイティブ拡張にも応用できそうだな、と思いました。

明日は @mita2 さんです!

*1:DBI による XS ドライバメソッドの呼び出しは高度に最適化されており、高速

*2:正確には開始の引用符を書き込んだ後に mysql_real_escape_string_quote() が実行される