SQLite3::escapeString()はバイナリセーフではないためnull文字を含む文字列を渡すと壊れますが、代わりの関数はなく、プリペアドステートメントを使う以外の方法が実質的にありません。

バグ報告はされている

SQLite3::escapeString()がバイナリセーフではないことは数年前にバグ報告されていました。

https://bugs.php.net/bug.php?id=63419
https://bugs.php.net/bug.php?id=62361

しかし、解決には至らずそのままになっています。

SQLite3の本体にエスケープ関数がない

SQLite2のsqlite_escape_string()がバイナリセーフなので後退しているようにも見えますが、SQLite本体側で「エスケープは不要」とみなされたというのが本質のようです。

SQLite2本体がテキストデータしか取り扱えずバイナリデータは疑似的に取り扱っていたのに対し、SQLite3がネイティブにバイナリデータに対応したことで、エンコード処理そのものがいらなくなったというのがSQLite本体の態度です。

http://sqlite.1065341.n5.nabble.com/Is-it-mandatory-to-use-sqlite-encode-binary-amp-sqlite-decode-binary-to-store-data-structures-imagess-td63311.html

sqlite_encode_binary() and sqlite_decode_binary() are legacy SQLite Version
2 interfaces which convert binary data to and from zero-terminated strings
so that the binary can be stored in a text-only database.

SQLite3 supports binary data natively and does not require any such
encoding.

SQLite3 has been the preferred version of SQLite for 8 years now. I wasn’t
aware that anybody was still using SQLite version 2.

引用の発言者であるRichard Hipp氏はSQLiteの作者さんです。

作者さんが「もうsqlite_encode_binary()は要らんよね」ということですが、PHPのsqlite_escape_string()は実装としてヌル文字を含む場合”のみ”sqlite_encode_binary()を呼び出しています。

https://github.com/php/php-src/blob/PHP-5.3.29/ext/sqlite/sqlite.c#L3153

つまりsqlite_escape_string()は「テキストデータ(シングルクォート)をエスケープする」「バイナリデータをエスケープする」という2つの役割を負っていました。それがSQLite3::escapeString()になって後者の役割は失せてしまい、バイナリセーフではなくなったという状況ができあがりました。

プリペアドステートメント以外の方法が現実的にない

SQLite3本体がエスケープ関数を用意していないため、PHPもsqlite_encode_binary()相当の関数を用意しにくい状況があります。

ではSQLite3コマンドでバイナリデータを入れる方法を自前実装すればいいじゃないかということになりますが、SQLite3でのBLOBリテラル表現は「X'FFFFFFF……'」のようにシングルクォートの前にXを書いて16進表記するという方法です。シングルクォートの中だけエスケープするsqlite_escape_string()との互換性が取れません。

結局sqlite_escape_string()の呼び出し側を修正する必要があり、ならプリペアドステートメント使ったほうがいいじゃないということになります。

方法は全くないわけではなさそう

SQLite2から3へのデータ移行方法は、マニュアル http://www.sqlite.org/version3.html の「New File Format」にある方法になります。

sqlite OLD.DB .dump | sqlite3 NEW.DB

つまりSQLite2のダンプファイルをSQLite3に入れるということです。これで正しくバイナリデータが移行できるのであれば、SQLite2のBLOBリテラル表現がSQLite3でも通用するということになります。

SQLite3でX'FFFFFFF……'の表現をした時、.dumpコマンドでのダンプはX”表記で出てきます。しかし、上記移行コマンドでバイナリデータを入れたSQLite3のDBダンプはX”表記ではなくバイナリのまま出てきます。つまり必ずしもBLOBで保持しているわけではく、stringとして持っていそうです。

そのバイナリデータはSQLite2のものとちょっと違い、2と3のダンプINSERT文のdiffを取ると差分が出てきてしまいます。これがデータ破壊であれば何かしらドキュメンテーションされていそうですが、見当たらないので、邪道であると知りつつもこれからその辺探ってみようと思います。

※追記:探りました

参考URL

痛い、痛い……。