刹那(せつな)の瞬き

Willkömmen! Ich heiße Setsuna. Haben Sie etwas Zeit für mich?

UbuntuでodbcクレートとMS版ODBCドライバで日本語を扱う

Rust の odbc クレートから SQL Server 2019 on Linux に接続する件ですが、私の環境では MS 版 ODBC ドライバで接続すると期待通りに動作しませんでした。

接続は成功するし、英文のサンプルは動作するのですが、クエリ文字列に日本語が含まれていると、文字化けやパニックが発生してしまいます。

この記事について、とりあえずと言うか、一応の解決策が見つかりました。
データベース接続の前に、次のコードを追加します。

unsafe {
    libc::setlocale(libc::LC_ALL, std::ffi::CString::new("").unwrap().as_ptr());
}

これは C言語ロケール情報を設定する setlocale(LC_ALL, "");を Rust で記述しているだけです。これで日本語を含むクエリが通るようになりました。

正直なところ、ロケールを設定すると副作用が心配ですが、まずは一歩前進です。
なお、""の部分は"ja_JP.UTF-8"、敢えて"C.UTF-8""en_US.UTF-8" にしても、日本語を含むクエリは通ります。
なので、何かあれば、その都度考える事にしました。

以下に、私が試したコードと実行結果を貼っておきます。

ソースコード : main.rs
extern crate libc;
extern crate odbc;
use odbc::*;

fn main() {
    unsafe {
        libc::setlocale(libc::LC_ALL, std::ffi::CString::new("").unwrap().as_ptr());
    }
    let env = create_environment_v3().unwrap();
    //let conn_str = "Driver={FreeTDS};ServerName=mssql-server;UID=sa;PWD=abcd1234$;Database=NorthwindJ";
    let conn_str = "Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$;Database=NorthwindJ";
    let conn = env.connect_with_connection_string(conn_str).unwrap();
    println!("\n確認 #1");
    {
        let sql_cmds = vec![ 
            "SELECT 1 + 2",     // 整数演算
            "SELECT 1.0 / 3",   // 小数演算
            "SELECT NULL",      // NULL
            "SELECT '絵文字の笑顔は😀 ビールは\u{1F37A}です。'",    // varchar
            "SELECT N'絵文字の笑顔は😀 ビールは\u{1F37A}です。'",   // nvarchar
        ];
        for qry in &sql_cmds {
            let stmt = Statement::with_parent(&conn).unwrap();
            let mut stmt = match stmt.exec_direct(qry).unwrap() {
                Data(stmt) => stmt,
                NoData(_) => panic!("結果セットは空"),
            };
            if let Some(mut cursor) = stmt.fetch().unwrap() {
                match cursor.get_data::<&str>(1).unwrap() {
                    Some(val) => println!("スカラ値: {}", val),
                    None => println!("NULL値を取得"),
                }
            }
        }
    }
    println!("\n確認 #2");
    {
        let stmt = Statement::with_parent(&conn).unwrap();
        let mut stmt = match stmt.exec_direct(
                "SELECT 社員コード,氏名,入社日 FROM 社員 ORDER BY 社員コード DESC"
            ).unwrap() {
            Data(stmt) => stmt,
            NoData(_) => panic!("結果セットは空"),
        };
        let cols = stmt.num_result_cols().unwrap();
        while let Some(mut cursor) = stmt.fetch().unwrap() {
            for i in 1..(cols + 1) as u16 {
                let val = cursor.get_data::<&str>(i).unwrap().unwrap();
                print!(" | {}", val);
            }
            println!(" |");
        }
    }
    conn.disconnect().unwrap();
} 

Cargo.toml の [dependencies] には、libc = "*"odbc = "*"を追加しました。
現在のビルドでは、libc-0.2.69odbc-0.16.1が使われてます。

・実行結果

f:id:infinity_volts:20200430182148p:plain

ちなみに、FreeTDS の ODBC ドライバの場合ですが、前述のソースコードの内容であれば、ロケール設定があってもなくても、この画面の結果になります。

※補足

少し残念な事に、MS 版 ODBCドライバから接続すると、ロケール設定をしていても、日本語のカラム名が文字化けします。

Statement::describe_col() でカラム名を取得する際に発生してるのですが、カラム名を英数字にすれば正常動作するので、実務ではほぼ問題ないはずです。

もし、気になるのであれば、文字化けしない FreeTDS の ODBC ドライバで接続する事になります。

※補足の補足 2020.5.24

カラム名の件について、私的な対応で回避してたのを忘れてました。
odbc クレートのコードを 1ヶ所修正すれば、MS 版 ODBC ドライバでも日本語カラム名が表示されます。

私は odbc-0.16.1/src/statement/mod.rs の 299行目、name_length の値を * 4 にして回避しました。

name: ::environment::DB_ENCODING.decode(&name_buffer[..((name_length * 4) as usize)]).0

付近に似たようなコードがもう 1 行ありますが、そちらはノータッチです。

これは現在最新版の odbc-0.17.0 で試しても同様でした。
odbc-iter でもこの対応をしないと Segmentation Fault になるので、仕方なしです。

でもまあ .cargo にあるコードを書き換えるくらいなら、素直に FreeTDS を使う方が良さそうですね。

結合文字ではない絵文字の件だけでも何とかなれば良いのですが。