Prepared Statement関連のプロトコルについて

検索結果を返す際に使用する"Fieldパケット"の構造は以下(MySQL 4.1以上).

VERSION 4.1
Bytes                      Name
-----                      ----
n (Length Coded String)    catalog
n (Length Coded String)    db
n (Length Coded String)    table
n (Length Coded String)    org_table
n (Length Coded String)    name
n (Length Coded String)    org_name
1                          (filler)
2                          charsetnr
4                          length
1                          type
2                          flags
1                          decimals
2                          (filler), always 0x00
n (Length Coded Binary)    default

この"charsetnr"にキャラクタセットの情報が入っているわけで.例えばcp932なら95番か96番.Collation(文字照合順位)をデフォルトにするかbinaryにするかで変わるはずだけど.

で,Server-Side Prepared Statementでパラメータをサーバに送信する時に使用する"Parameterパケット"の構造は以下.

Bytes                   Name
-----                   ----
2                       type
2                       flags
1                       decimals
4                       length

type:                Same as for type field in a Field Packet.
flags:               Same as for flags field in a Field Packet.
decimals:            Same as for decimals field in a Field Packet.
length:              Same as for length field in a Field Packet.

え〜っと,,,,,重要なことに気づいてしまいました.やっぱりね.どーもC/Jのソース読んでもしっくりこなかったわけだ.

Server-Side Prepared Statementで使用するプロトコルの中に,パラメータのキャラクタセットを指定する項目がありません.

特定のカラムに対してのみ任意のキャラクタセットを指定して何か(SELECT,INSERT,UPDATEあるいはCONVERT関数など)をする場合にイントロデューサー(Introducer,例:"_sjis")をStatementオブジェクトを使用する場合だったら使えるわけだけど,PreparedStatementを使用する場合には使えないということがこれで判明.

ついでに言うと,Prepared StatementのSQL文そのものに対してもこのイントロデューサーは使用できない.

mysql> PREPARE stmt1 FROM 'SELECT ?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql> SET @a='abc';
Query OK, 0 rows affected (0.00 sec)

mysql> EXECUTE stmt1 USING @a;
+-----+
| ?   |
+-----+
| abc |
+-----+
1 row in set (0.00 sec)

mysql> PREPARE stmt2 FROM 'SELECT _utf8 ?';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL
 server version for the right syntax to use near '?' at line 1
mysql>
mysql> SELECT _utf8 'abc';
+-----+
| abc |
+-----+
| abc |
+-----+
1 row in set (0.00 sec)

"SELECT _utf8 'abc'"そのものは通常のクエリ実行であれば動くのに,Prepared Statementにしようとするとmysqlclientが構文エラーだと言う.構文エラーってのもまた違うと思うけど.

Connector/Jの場合はPrepared Statement用のSQL文にイントロデューサーが含まれているとServer-Side Prepared StatementではなくClient-Side Prepared Statementが使用される.正確には最初Server-Side Prepared StatementでやろうとしてSQLExceptionが発生するのでそれを内部的にcatchしてClient-Side Prepared Statementで再試行するのでそうなるということ.

try {
    pStmt = new com.mysql.jdbc.ServerPreparedStatement(this,
            nativeSql, this.database);
    if (this.getCachePreparedStatements()
            && sql.length() < getPreparedStatementCacheSqlLimit()) {
        ((com.mysql.jdbc.ServerPreparedStatement) pStmt).isCached = true;
    }
} catch (SQLException sqlEx) {
    // Punt, if necessary
    if (getEmulateUnsupportedPstmts()) {
        pStmt = clientPrepareStatement(nativeSql);

        if (getCachePreparedStatements()
                && sql.length() < getPreparedStatementCacheSqlLimit()) {
            this.serverSideStatementCheckCache.put(sql,
                    Boolean.FALSE);
        }
    } else {
        throw sqlEx;
    }
}

catchのところでsqlEx.printStackTrace()するように1行追加し,適当なコードを使って動かしてみると

com.mysql.jdbc.exceptions.MySQLSyntaxErrorException: You have an error in your SQL syntax; 
check the manual that corresponds to your MySQL server version for the right syntax to use near '?, _utf8 ?)' at line 1
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:937)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3021)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1680)
    at com.mysql.jdbc.ServerPreparedStatement.serverPrepare(ServerPreparedStatement.java:1345)
    at com.mysql.jdbc.ServerPreparedStatement.<init>(ServerPreparedStatement.java:317)
    at com.mysql.jdbc.Connection.prepareStatement(Connection.java:3580)

んでもってGeneral Logには,"INSERT INTO t1 (c1, c2) VALUES (?, ?)"で実行して普通にServerPreparedStatementが動作している場合には,

55 Prepare     [1] 
55 Execute     [1] INSERT INTO t1 (c1, c2) VALUES ('おはよう','こんにちは')

てな感じになるのが,この"INSERT INTO t1 (c1, c2) VALUES (_sjis ?, _utf8 ?)"とかで実行すると,

57 Query       INSERT INTO t1 (c1, c2) VALUES (_sjis 'おはよう', _utf8 'こんにちは')

という具合にClient-Side Prepared Statement(サーバに対しては普通のStatement)として実行される.

※つまりmysqlclientの問題ではなく,サーバのパーサがPrepared Statementのときにこれを弾く実装に今はなっているってことね.
※これだとパラメータではなくPrepared Statement用のSQL文をいじることで対応,という方法もできないね.

ちょっと脱線してしまったが,さあ困った.ServerPreparedStatementに対してはNATIONAL CHARACTER対応実装できないじゃん.これはMark Matthews氏に説明しないといかんね.