Hatena::Grouprubyist

Rubyで遊ぶよ

 | 

2009-10-14

sqlite3-rubyのエンコーディング問題についてメモ

11:29

簡単なメモ。

こういう問題がある。


SQLite3 のドキュメント (強調したところ) によると、

Support for UTF-8 and UTF-16

The new API for SQLite 3.0 contains routines that accept text as both UTF-8 and UTF-16 in the native byte order of the host machine. Each database file manages text as either UTF-8, UTF-16BE (big-endian), or UTF-16LE (little-endian). Internally and in the disk file, the same text representation is used everywhere. If the text representation specified by the database file (in the file header) does not match the text representation required by the interface routines, then text is converted on-the-fly. Constantly converting text from one representation to another can be computationally expensive, so it is suggested that programmers choose a single representation and stick with it throughout their application.

In the current implementation of SQLite, the SQL parser only works with UTF-8 text. So if you supply UTF-16 text it will be converted.This is just an implementation issue and there is nothing to prevent future versions of SQLite from parsing UTF-16 encoded SQL natively.

When creating new user-defined SQL functions and collating sequences, each function or collating sequence can specify it if works with UTF-8, UTF-16be, or UTF-16le. Separate implementations can be registered for each encoding. If an SQL function or collating sequences is required but a version for the current text encoding is not available, then the text is automatically converted. As before, this conversion takes computation time, so programmers are advised to pick a single encoding and stick with it in order to minimize the amount of unnecessary format juggling.

SQLite is not particular about the text it receives and is more than happy to process text strings that are not normalized or even well-formed UTF-8 or UTF-16. Thus, programmers who want to store IS08859 data can do so using the UTF-8 interfaces. As long as no attempts are made to use a UTF-16 collating sequence or SQL SQLite is not particular about the text it receives and is more than happy to process text strings that are not normalized or even well-formed UTF-8 or UTF-16. Thus, programmers who want to store IS08859 data can do so using the UTF-8 interfaces. As long as no attempts are made to use a UTF-16 collating sequence or SQL function, the byte sequence of the text will not be modified in any way.

SQLite Version 3 Overview
  • テキストは UTF-8UTF-16 で保存できる。
  • もし UTF-16 で保存したテキストを UTF-8 で要求したら、内部で変換される。逆も然り。
    • (UTF-8 で要求とはどういうことだろう?)
  • SQL パーサーは UTF-8 しか受け付けない。
    • 将来は UTF-16 も使えるようにするかも。
  • 実際のところ、UTF-8UTF-16 じゃなくても気にせず受け入れている。

こっちも参考。


現在の最新版はどんな感じか

去年の11月にjamis さんが開発から手を引いて、現在は luislavena さんのところにあるらしい。

しかしけっこうフォークされてて、多くはマージされてないもよう。(たぶん)

f:id:edvakf:20091014094017p:image:w600 f:id:edvakf:20091014093933p:image:w600

Tracker はほぼ機能していないのかと思っていたのだけど、ごく最近あった書き込みにも対応されているっぽい。

とりあえず luislavena さんの master を見る限り、エンコーディングのことは何も気にされてない。


修正するとしたら

  • ASCII-8BIT 文字列で返されて自分で force_encoding しないといけないのは直したい。

基本的にはデータベースのエンコーディングを UTF-8 に決め打ちして、

  • SQL 文、プレースホルダー、共に .encode("UTF-8") する。
  • 結果を .force_encoding("UTF-8") する。

UTF-8 ではないデータベースを開きたいときのために、

  • Database.open 時にエンコーディングを指定できるようにする。
    • エンコーディング指定なしは UTF-8 に。

それだと現在との互換性がなくなるので、とりあえず

  • Database.open 時に「変換しない」オプションを付けるとエンコーディングを変換しないように。(今とまったく同じ)

これで急場を凌げるようにしたらいいんじゃないかな。しかしこのオプションは Not Recommended ということにする。


.force_encoding("UTF-8").encode(Encoding.default_internal) のほうがいいかも? よくわからん。

この結果、各種ライブラリは

  • 基本UTF-8を返せばよい、入力もUTF-8を期待
  • より親切なライブラリはdefault_internalで返す。入力はencodingを見て対処
Re: Encoding.default_internal のためのパッチ

default_internal にしてもいいけど、パフォーマンスを考えるとどうなんだろう?


utf16フラグは

utf16 オプションが付いているかどうかは気にしなくてもいいような気がする。なぜなら、

require 'sqlite3'
idb = SQLite3::Database.open('foobar.db', {:utf16 => true})
db.execute('CREATE TABLE tbl (txt TEXT)')
db.execute('INSERT INTO tbl VALUES (?)', 'aiueo あいうえお'.encode('UTF-16LE'))
db.execute('SELECT * FROM tbl')
#=> [["a"]]

↑ちゃんと保存できてない。(UTF-16 にすると NULL 文字が入るので、そこで切られる)

The new API for SQLite 3.0 contains routines that accept text as both UTF-8 and UTF-16 in the native byte order of the host machine. とあるのにマシンのエンコーディングである UTF-16LE をそのまま入れてもだめというのはどういうことだろう?

そんなわけで、:utf16 => true の場合でも引数 UTF-8 にして、内部の変換にまかせるのがよさそう。

変換はちゃんとされているっぽい。(データベースファイルを strings コマンドで見た)

出力時も UTF-8 バイト列に変換されるっぽい。ただしエンコーディングは ASCII-8BIT。

require 'sqlite3'
db = SQLite3::Database.open('foobar.db', {:utf16 => true})
db.execute('CREATE TABLE tbl (txt TEXT)')
db.execute('INSERT INTO tbl VALUES (?)', 'あいうえお')
p db.execute('SELECT * FROM tbl')
#=> [["\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A"]]
puts db.get_first_value('SELECT * FROM tbl')
#=>あいうえお
puts db.get_first_value('SELECT * FROM tbl').encoding
#=>ASCII-8BIT

実際のところ、utf16 フラグを付けて open したファイルの名前は UTF-16 と解釈される (微妙に違う?) ので、

db = SQLite3::Database.open('foobar.db', {:utf16 => true})

とやってファイルを開くと、UTF-8 のターミナルでは「潦扯牡搮b」("\xE6\xBD\xA6\xE6\x89\xAF\xE7\x89\xA1\xE6\x90\xAEb") になってしまう。foobar.db を開いたつもりが foobar.db じゃないのは混乱の元だと思う。これも直したほうがいいのかも。どうなんだろう?

db = SQLite3::Database.open('foobar.db'.encode('UTF-16LE'), {:utf16 => true})

これだったら「foobar.dbĀ?」("foobar.dbA\xCC\x84\x01") というファイルができる。よくわからん。

まあそんなわけで、たぶん sqlite3-ruby で utf16 オプションを使っている人は皆無なんじゃないかな?


Pythonはどうなってるんだろう?

CPython の SQLite ライブラリは C で書かれてる。

text_factory というのを指定するらしい。

class TextFactoryTests(unittest.TestCase):
    def setUp(self):
        self.con = sqlite.connect(":memory:")

    def CheckUnicode(self):
        austria = unicode("Österreich", "latin1")
        row = self.con.execute("select ?", (austria,)).fetchone()
        self.assertTrue(type(row[0]) == unicode, "type of row[0] must be unicode")

    def CheckString(self):
        self.con.text_factory = str
        austria = unicode("Österreich", "latin1")
        row = self.con.execute("select ?", (austria,)).fetchone()
        self.assertTrue(type(row[0]) == str, "type of row[0] must be str")
        self.assertTrue(row[0] == austria.encode("utf-8"), "column must equal original data in UTF-8")

    def CheckCustom(self):
        self.con.text_factory = lambda x: unicode(x, "utf-8", "ignore")
        austria = unicode("Österreich", "latin1")
        row = self.con.execute("select ?", (austria.encode("latin1"),)).fetchone()
        self.assertTrue(type(row[0]) == unicode, "type of row[0] must be unicode")
        self.assertTrue(row[0].endswith(u"reich"), "column must contain original data")

    def CheckOptimizedUnicode(self):
        self.con.text_factory = sqlite.OptimizedUnicode
        austria = unicode("Österreich", "latin1")
        germany = unicode("Deutchland")
        a_row = self.con.execute("select ?", (austria,)).fetchone()
        d_row = self.con.execute("select ?", (germany,)).fetchone()
        self.assertTrue(type(a_row[0]) == unicode, "type of non-ASCII row must be unicode")
        self.assertTrue(type(d_row[0]) == str, "type of ASCII-only row must be str")

    def tearDown(self):
        self.con.close()

self.con.text_factory = sqlite.OptimizedUnicode だと、文字列が ascii 範囲内なら str にして、それ以外なら unicode にすると。

UTF-16 のことについてはまったくノータッチっぽい。


実は

もうほとんどパッチは出来てるんだけど、調べてたらちょっと変えたほうがいいところもあると思ったので、もうちょっと弄ろうと思う。

 |