刹那(せつな)の瞬き

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

UbuntuでRustからSQLServerに接続できたけど道半ば

以前、Microsoft が Rust に注目うんぬんの記事を読んで、Rust に興味を持ちました。

SQL Server 公式サイトの接続サンプルに Rust が加わる事を期待してるのですが、残念ながら今のところ存在しません。

気になったので、Ubuntu 上の Rust から SQL Server on Linux に接続してみました。

1. 環境

  • Ubuntu Desktop 18.04.4 LTS 日本語Remix (Linux Kernel 5.3.0-42-generic)
  • SQL Server 2019 (RTM-CU3) (KB4538853) - 15.0.4023.6 (X64) on Linux
  • ODBC ドライバ version 17.5.2
  • unixODBC 2.3.7
  • rustc 1.41.1
  • NorthwindJ データベース ※Node.js と Qt5 から正常に接続できる事を確認済み。

2. Crate odbc

MS 版 ODBC ドライバを使いたいので、odbc クレートで接続してみました。

(1) ソースコード

評価のため、https://lib.rs/crates/odbc のサンプルを元に DSN-less で接続するように改変してます。

・main.rs :

extern crate odbc;
use odbc::*;

fn main() {
    match connect() {
        Ok(()) => println!("Success"),
        Err(diag) => println!("Error: {}", diag),
    }
}

fn connect() -> std::result::Result<(), DiagnosticRecord> {
    let env = create_environment_v3().map_err(|e| e.unwrap())?;
    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)?;
    println!("Connected");
    execute_statement(&conn)
}

fn execute_statement<'env>(conn: &Connection<'env, odbc_safe::AutocommitOn>) -> Result<()> {
    let stmt = Statement::with_parent(conn)?;
    let sql_text = "SELECT * FROM 社員";
    println!("{}", &sql_text);
    match stmt.exec_direct(&sql_text)? {
        Data(mut stmt) => {
            let cols = stmt.num_result_cols()?;
            while let Some(mut cursor) = stmt.fetch()? {
                for i in 1..(cols + 1) {
                    match cursor.get_data::<&str>(i as u16)? {
                        Some(val) => print!(" {}", val),
                        None => print!(" NULL"),
                    }
                }
                println!("");
            }
        }
        NoData(_) => println!("Query executed, no data returned"),
    }
    Ok(())
}
(2) 実行結果

ビルドして実行するとデータベース接続は成功するものの、クエリの実行に失敗します。

f:id:infinity_volts:20200317171140p:plain

よく見る「文字化け」なので、日本語を含まなければ大丈夫そうです。

試しに sql_text をSELECT * FROM sysobjects WHERE xtype = 'u'にしてみると、こちらは期待通りに動作します。

f:id:infinity_volts:20200317171513p:plain

結果セットの文字列は「文字化けしていない」ので、クエリ文字列を扱う箇所の問題みたいです。

(3) 対策 ※したけど改善なし

対策したいのですが、可能な手段がほとんどありません。

  1. create_environment_v3()の代わりにcreate_environment_v3_with_os_db_encoding("utf8", "sjis") 等を試す。
  2. exec_direct()の代わりにexec_direct_bytes()を試す。

utf8 と sjis の組み合わせと、encoding_rs クレートでエンコーダを生成してバイト列変換、を総当たりで試しましたが、クエリ文字列の「文字化け」は改善しませんでした。

Windows OS だと、結果が異なるかもしれません。

※2020.4.30追記 libc::setlocale を設定すると期待通りに動作するようになりました。
→ 詳細は こちらの記事 で! 

(4) 所感

現状、テーブル名や列名は英数字にするとしても、WHERE 句に条件を記述したい場合は対応できません。

この後、r2d2 で接続プールを試したり、Diesel で ORM を試したかったのですが、とりあえず保留とします。

もし使うなら、ここは素直に PostgreSQL かなぁ。

(5) FreeTDS なら正常に動作 ※2020.3.18 追記

残念だけど、MS 版 ODBC ドライバを諦めて、FreeTDS の ODBC ドライバに変更したら、あっさり動作しました。日本語を含むクエリ文字列でも正常に処理されます。

$ sudo apt install freetds-dev tdsodbc

/etc/freetds/freetds.confにサーバ [mssql-server] を追加

[mssql-server]
host = localhost
port = 1433
tds version = 7.4
client charset = UTF-8

/etc/odbcinst.iniにドライバ [FreeTDS] を追加

[FreeTDS]
Description=FreeTDS Driver
Driver=/usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so
Setup=/usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so

~/.odbc.iniにデータソース [Northwind] を追加

[Northwind]
Driver=FreeTDS
Description=NorthwindJ sample DB for FreeTDS
ServerName=mssql-server
DataBase=NorthwindJ

後は、接続文字列をlet conn_str = "DSN=Northwind;UID=sa;PWD=abcd1234$";に変更するだけです。

本来はテンプレートファイルを作成して、odbcinst でインストールするべきですが、面倒だったので、各ファイルに直接追記してます。

これで正常に動作するということは、MS 版 ODBC ドライバに client charset = UTF-8 が通知されないのかなぁ。
Qt5 では MS 版 ODBC ドライバで問題ないんだけどなぁ。
何れにしても、ひとまず「保留」とします。
 

3. Crate tiberius 編

ODBC ではなく native TDS になりますが、tiberius クレートも試してみました。

私のスキルの問題で古い futures が必要なバージョンでの検証になります。

  • tiberius = { version = "0.3.2", default-features=false,features=["chrono"] }
  • chrono = "0.4.11"
  • futures = "0.1.18"
  • futures-state-stream = "0.1"
  • tokio-core = "0.1.17"

これらを Cargo.toml の [dependencies] に設定をしないと動作しません。

(1) ソースコード

https://docs.rs/tiberius/0.3.2/tiberius/ にあるサンプルの動作環境を再現できなかったので、Stack Overflow にあるこちらのコードを参考に改変しました。

・main.rs :

extern crate futures;
extern crate tokio_core;
extern crate tiberius;
extern crate chrono;

use futures::Future;
use futures_state_stream::StateStream;

fn main() {
    let mut core = tokio_core::reactor::Core::new().unwrap();
let mut v: Vec<String> = Vec::new();
let conn_str = "server=tcp:localhost,1433;trustservercertificate=yes;Database=NorthwindJ;UID=sa;PWD=abcd1234$"; let future = tiberius::SqlConnection::connect(conn_str).and_then(|conn| { conn.simple_query("SELECT TOP 5 社員コード, 氏名, 誕生日 FROM 社員").for_each(|row| { let val: i32 = row.get(0); // 社員コード int let name: &str = row.get(1); // 氏名 nvarchar let birthday: chrono::NaiveDateTime = row.get(2); // 誕生日 datetime v.push(format!("{} - {} - {}", val, name, birthday.format("%Y/%m/%d").to_string())); Ok(()) }) });
core.run(future).unwrap(); for row in &v { println!("{}", row); } }
(2) 実行結果

ビルドして実行すると、こちらは期待通りに動作します。

f:id:infinity_volts:20200317181008p:plain

(3) 所感

動作させるまで大変でしたが、無事に接続できてよかったです。

今後は、古い futures ではなく、async / await になっていくようですね。

私はまだ TRPL を中心に学習してる段階なので、Rust 2018 Edition に追いついたら、改めて試してみたいと思います。