刹那(せつな)の瞬き

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

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

時折この記事が見られていますが、別の方法があるので紹介します。

2022-05-15 MS版ODBCドライバは odbc-api クレートで

現状 macOS, Linux 環境において、Rust で MS 版 ODBC ドライバを利用したい場合は、odbc クレートではなく、odbc-api クレートをお奨めします。

std::thread による並列処理には r2d2 / r2d2_odbc_api が利用できます。

また、本来 tokio, async-std ランタイムの非同期タスクには対応していませんが、 deadpool / deadpool_r2d2 / r2d2_odbc_api の組み合わせで利用可能です。

odbc クレートの現状

最終版が 0.17.0 のまま、2年程更新されていません。
接続プールの rd2d_odbc も同様です。

何が困るかというと、関連するクレートの依存関係が解消されていません。

使用するクレート 依存するバージョン
最終版? 0.17.0
r2d2_odbc 0.16.1
odbc-iter 0.13.0

各バージョンでコンパイルが通らない程度の変更があるので注意が必要です。
私は r2d2_odbc に合わせて、バージョン 0.16.1 固定と割り切りました。
現在も FreeTDS 版 ODBC ドライバ tdsodbc との組み合わせは良好です。

そして、元記事の内容は odbc-api クレートを利用する場合、一切不要です。
既に無意味ですが、私が足掻いた記録として残して置きます。

元記事 ... 既に役割を終えたので、ただの記録

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 を使う方が良さそうですね。

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