刹那(せつな)の瞬き

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

炊飯器を一升炊きから5.5合炊きに買い換えた

先日、炊飯器が壊れました。
自己診断のエラー表示によると、故障はフタの開閉部の断線との事。

母が炊事してた頃から使ってたので、ついに来たかー、って感じです。
少なくとも8年くらいは使用してたので悔いはないかな。

念の為、修理の相場を調べたら、費用は1.6〜2万円らしい。
それなら新品買う方が良いよね。

....

次のサイズはどうするかちょっとだけモメました。

田舎なので、以前は季節のイベントの度に一升炊いてたようです。
が、私はそこまでするつもりはなく。
好きで炊事してる訳じゃないので断固拒否です。

モメた割に、近所の家電屋さんでは、ほとんどが5.5合炊きで3合が少々。
一升タイプは各メーカー1種類あるだけでした。これじゃモメ損だよ。

....

結局購入したのは5.5合炊きタイプです。

一人暮らしの頃も敢えて5.5合炊きを使ってたので、慣れたものです。
使い勝手が良くなって、とても満足してます。

無駄にでかい釜が嫌だったのですが・・・
またひとつ、見慣れた母の道具が消えたかと思うと、もの寂しいもんですね。

Ubuntuでodbc-iterクレートを試したら便利だった

前回までで、Rust の odbc クレートから SQL Server 2019 on Linux に接続して、日本語を含むクエリが処理できるようになりました。

次は日付や数値等の評価をしてみようと、データベース型の扱いについて調べてたら、odbc-iter クレートの存在を知りました。

odbc-iter クレートは、内部で odbc クレートを使用しています。
現状では、日付時刻は chrono、数値は rust_decimal の各クレートも併用してるとの事なので、それならば私が自力で寄せ集めなくても済むし、試してみようかと。

サンプルやソースコードを読むと、odbc クレートよりも簡略化して書けそうです。
以下に、比較用に書いたコードと実行結果を貼っておきます。

・Cargo.toml への追記
[dependencies]
libc = "*"
odbc-iter = { version = "0.2.5", features = ["rust_decimal"] }
odbc = "0.13.0"
ソースコード : main.rs

とりあえず、単一の値を返却するクエリで試してみます。

extern crate libc;
extern crate odbc_iter;
extern crate odbc;

use odbc_iter::{Odbc, Value};
use odbc::*;

fn main() {
    unsafe { libc::setlocale(libc::LC_ALL, std::ffi::CString::new("").unwrap().as_ptr()); }
    let conn_str = "Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$;Database=my_test_db";
    //let conn_str = "Driver={FreeTDS};ServerName=mssql-server;UID=sa;PWD=abcd1234$;Database=my_test_db";

    // for odbc crate
    let env = create_environment_v3().unwrap();
    let conn = env.connect_with_connection_string(conn_str).unwrap();

    // for odbc-iter crate
    let mut connection = Odbc::connect(conn_str).expect("failed to connect to database");
    let mut db = connection.handle();
    
    let sql_cmds = vec![ 
        "SELECT '絵文字の笑顔は😀 ビールは\u{1F37A}です。'",    // varchar
        "SELECT N'絵文字の笑顔は😀 ビールは\u{1F37A}です。'",   // nvarchar
        "SELECT NULL",      // NULL
        "SELECT 1 + 2",     // 整数演算
        "SELECT 1.0 / 3",   // 小数演算
        "SELECT CAST('922337203685477.5807' as decimal(19,4))",
        "SELECT CAST('-922337203685477.5808' as decimal(19,4))",
]; println!("\n---- odbc crate"); { 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---- odbc-iter crate"); { for qry in &sql_cmds { match db.query::<Option<Value>>(qry).expect("failed to run query") .single().expect("failed to fetch row") { Some(val) => println!("スカラ値: {}", val), None => println!("NULL値を取得"), } } } }

このコードでは NULL 値を扱いたいので、Option<Value>で受けてます。

NULL 値が不要なら、odbc-iter クレートのサイトに掲載されているサンプルのように、より簡潔に書けます。サンプルでは型指定になってますが、型を指定したくなければ、Valueで受けるのも可能です。

・実行結果

f:id:infinity_volts:20200506234035p:plain

小数の出力結果が少し異なりますが、ほぼ同じです。

補足

今回の件とは別に、ODBC の評価ではいつも同じようなコードを書いてます。

実は、既に odbc クレート用に評価コード書いて検証を済ませてました。
それを odbc-iter クレート用に書き換えたら、思ってた以上にデータベース操作が楽になりました。

しばらく odbc-iter クレートで過すのもアリかな、と思うくらい気に入ったので、もっと利用者が増える事を願ってます。

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

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

上海(ゲーム)が面白くて飽きない

上海(シャンハイ)というパズルゲームがあります。

昔、PC-9801 用のタイトルで初めて上海に触れて、それ以来どハマりしました。

そんな私が GNOME上海の存在を知ってしまった日は、ひとりで狂喜乱舞。
勢い余って、当時使用していた Unix系OS のデスクトップ環境をすべて GNOME に切り換えました。私、KDE派だったのになー

今でも Ubuntu をインストールする度に取り敢えずプレイするのは、私のお約束です。
私の Ubuntu 歴は 8.04 からなので、既に12年です。飽きる気配がまるでありません。

....

頭をリセットしたい時にプレイしてるのですが、稀にやる気スイッチが入ってしまうと、手が止まらなくなるのが悩ましいです。

シンプルかつ短時間で終わる為、中毒状態だとスコア更新に余念がありません。
2014年には上級で 2分を切るようになり、記念のスナップショットがこちらです。

f:id:infinity_volts:20200414173838p:plain

当時はいろんな意味で悩んでたので、余計に熱が入ってたようです。
ハイスコアは 1m 30s を切った記憶はあるのですが、エビデンスがないので無効ですね。

最近も少し現実逃避気味だったので、ついつい手を出してました。
今はもう落ち着いたので、いろんな意味でも大丈夫です。

....

ある日、元同僚の洋ゲー好きとゲームの話してたら、レトロなPCゲームの話になり、私が何気なく上海の話題を出しました。

そしたら、「何んすか、そのゲーム?初めて聞いたんですけどー」との塩対応。
えー、ソリティアマインスイーパは知ってるのに!?
さめがめ だって知ってるくせに!?
なのに、上海は知らないって何!

上海は Wikipedia にも記事があるくらいなので、ゲーマーなら誰もが普通に知ってると思ってました。

彼はアラサーだったので、もうその世代では知られてないのかな。
上海、面白いのになー

UbuntuでnanodbcからSQLServerに接続する #3 - 追試編

導入編では nanodbc をビルドして Ubuntu 環境にインストールしました。
ここでは、日本語を含むテーブル・カラム・クエリ等の扱いと、その他、気になる点について追試してみます。

対象は引き続き MS 版 ODBC ドライバです。

1. 追試用プロジェクトの準備

導入編で nanodbc をインストールした状態が前提です。
その続きとして、別途 nanodbc の共有ライブラリを利用する環境を整えます。

(1) 追試用ディレクト

既に work ディレクトリが存在するとして、新たに my_test ディレクトリを作成します。

$ cd ~/work
$ mkdir my_test
$ cd my_test
(2) ソースコード

my_test ディレクトリに、CMakeLists.txtmain.cppを作成します。

・CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(my_test CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(nanodbc REQUIRED)

add_executable(a.out main.cpp)
target_compile_options(a.out PRIVATE -g -Wall)
target_link_libraries(a.out PRIVATE nanodbc)

・main.cpp

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <nanodbc/nanodbc.h>

int main()
{
    std::locale::global(std::locale(""));   // for ODBC driver
    try
    {
        nanodbc::connection conn("Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$;Database=my_test_db");
        std::cout << "確認 #1"  << std::endl;
        {
            // varchar は文字化けする
            nanodbc::result rs1 = nanodbc::execute(conn, NANODBC_TEXT("SELECT '社員コード'"));
            rs1.next();
            std::cout << "varchar として処理 : [" << rs1.get<std::string>(0) << "]" << std::endl;
            // nvarchar は正常
            nanodbc::result rs2 = nanodbc::execute(conn, NANODBC_TEXT("SELECT N'社員コード'"));
            rs2.next();
            std::cout << "nvarchar として処理 : [" << rs2.get<std::string>(0) << "]" << std::endl;
            // NULL値
            nanodbc::result rs3 = nanodbc::execute(conn, NANODBC_TEXT("SELECT NULL"));
            rs3.next();
            std::cout << "NULL値を取得 : " << rs3.get<std::string>(0, "< null >を取得") << std::endl;
        }
        std::cout << "確認 #2" << std::endl;
        {
            // テーブルの準備
            nanodbc::execute(conn, NANODBC_TEXT("DROP TABLE IF EXISTS 社員"));
            nanodbc::execute(conn, NANODBC_TEXT(
                "CREATE TABLE 社員 (コード int, 氏名 nvarchar(40), 入社日 date, 備考 nvarchar(max))"
            ));
            // ローの準備
            std::vector<std::vector<std::string>> rec = {
                { "210", "成宮 真紀", "1991-04-01" },
                { "110", "加藤 泰江", "1990-04-01" },
                { "105", "森上 偉久馬", "1990-04-01" },
                { "304", "山本 雅治", "1989-04-01" },
                { "307", "小川 さよ子", "1987-04-01" },
                { "305", "青木 俊之", "1988-04-01" },
                { "207", "松沢 誠一", "1994-04-01" },
                { "107", "葛城 孝史", "1991-09-01" },
                { "204", "川村 匡", "1990-04-01" },
            };
            // プレースホルダを経由して、ローの値を INSERT
            nanodbc::statement stmt(conn);
            nanodbc::prepare(stmt, NANODBC_TEXT("INSERT INTO 社員 (コード,氏名,入社日) VALUES (?,?,?)"));
            for (const std::vector<std::string>& param : rec) {
                stmt.bind(0, param.at(0).c_str());
                stmt.bind(1, param.at(1).c_str());
                stmt.bind(2, param.at(2).c_str());
                nanodbc::execute(stmt);
            }
            // テーブル内容の表示
            nanodbc::string qry(NANODBC_TEXT("SELECT コード,氏名,入社日 FROM 社員 ORDER BY コード"));
            nanodbc::result rs = nanodbc::execute(conn, qry);
            // この時点で affected_rows() == -1
            std::cout << "結果 " << rs.affected_rows() << " 行, rowset size: " << rs.rowset_size() << std::endl;
            // カラム名表示
            std::cout << rs.column_name(0) << " | " << rs.column_name(1) << " | " << rs.column_name(2) << std::endl;
            // 取得ロー表示
            for (const auto& row : rs) {
                auto val = row.get<int>(0);
                auto name = row.get<nanodbc::string>(1);
                auto d = row.get<nanodbc::date>(2);
                std::cout << val << " | " << name << " | " << d.year << "." <<  d.month << "." << d.day << std::endl;
            }
            // この時点で affected_rows() == 取得ロー数
            std::cout << "結果 " << rs.affected_rows() << " 行, rowset size: " << rs.rowset_size() << std::endl;
        }
        std::cout << "確認 #3"  << std::endl;
        {
            // プレースホルダを経由して、ローの値を UPDATE
            nanodbc::statement stmt(conn);
            nanodbc::prepare(stmt, NANODBC_TEXT("UPDATE 社員 SET 備考=? WHERE コード=?"));
            // 生文字列リテラル
            std::string memo1 = R"(【自己紹介】
私の故郷は、富士五湖のひとつである本栖湖のほとりにあり、実家はキャンプ場を経営しています。

年中無休なので、子供の頃からよく手伝わされました。
特に夏のシーズンは忙しく、夏休みに家族で旅行に行った記憶はありません。
)";
            stmt.bind(0, memo1.c_str());
            stmt.bind(1, "210");
            nanodbc::execute(stmt);
            // 絵文字を含む文字列を連結 ※連結コストは無視
            std::list<std::string> sl;
            sl.push_back("【自己紹介】\n");
            sl.push_back("私は、スポーツが好きで、特にサッカー" u8"\U000026BD\U0001F945" "は見るのもするのも夢中になります。\n");
            sl.push_back("\n");
            sl.push_back("並行して、家が以前、エレクトーン" u8"\U0001F3B9" "の教室を開いていたので、楽器もよく演奏したりしていました。\n");
            std::string memo2;
            for (const auto& line : sl) {
                memo2 += line;
            }
            const int val = 110;
            stmt.bind(0, memo2.c_str());
            stmt.bind(1, &val);
            nanodbc::execute(stmt);
            // テーブル内容の表示
            nanodbc::string qry(NANODBC_TEXT("SELECT コード,氏名,備考 FROM 社員 WHERE 備考 IS NOT NULL"));
            nanodbc::result rs = nanodbc::execute(conn, qry);
            for (const auto& row : rs) {
                std::cout << row.get<int>("コード") << " : " << row.get<nanodbc::string>("氏名") << std::endl;
                auto prof = row.get<nanodbc::string>("備考");
                std::cout << ((prof.length() > 0) ? prof : "なし") << std::endl;
            }
        }
        std::cout << "確認 #4"  << std::endl;
        {
            // 氏名を限定して検索
            nanodbc::statement stmt(conn);
            nanodbc::string sql_text(NANODBC_TEXT("SELECT コード,氏名,入社日 FROM 社員 WHERE 氏名 LIKE ? OR 氏名 LIKE ?"));
            nanodbc::prepare(stmt, sql_text);
            stmt.bind(0, "%泰%");
            stmt.bind(1, "%川%");
            nanodbc::result rs = nanodbc::execute(stmt);
            // 結果セットを保存
            struct temp {
                int id;
                nanodbc::string name;
                nanodbc::string joined;
            };
            std::vector<temp> target;
            for (const auto& row : rs) {
                target.push_back({
                    row.get<int>("コード"),
                    row.get<nanodbc::string>("氏名"),
                    row.get<nanodbc::string>("入社日"),
                });
            }
            // 保存した順に表示
            std::cout << "1回目" << std::endl;
            for (const auto& d : target) {
                std::cout << d.id << " | " << d.name << " | " << d.joined << std::endl;
            }
            // 逆順で表示
            std::cout << "2回目" << std::endl;
            for (int i = target.size(); i > 0; i--) {
                const auto& d = target.at(i - 1);
                std::cout << d.id << " | " << d.name << " | " << d.joined << std::endl;
            }
        }
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

このソースコードは、私が Qt5 で書いたテストコードを部分的に流用してます。
その為、一部に不自然な記述もありますが、今回の追試には影響ないはずです。

(3) ビルド用ディレクトリの準備

my_test ディレクトリにビルド用のディレクトリを作成します。

$ pwd
/home/dareka/work/my_test
$ mkdir build
$ cd build

以降の操作は、このディレクトリで作業します。

2. ビルドと実行

(1) Makefile 生成と make

まずはプロジェクトをコンパイルして実行ファイルを生成します。

$ cmake ..
$ make
(2) 実行

ビルドが成功すると、a.outが生成されるので、これを実行します。
テスト編と同様に、実行時の接続先データベースは 'my_test_db' です。

$ ./a.out

3. 実行結果の確認

確認したい事を 4 つのブロックに分けて処理しました。
結果は、すべて想定内・期待通り、でした。 

(1) 単一の値を返すクエリの確認

std::cout << "確認 #1" << std::endl;以降のブロックです。

SELECT 文に値を直接指定して nanodbc::result で受けてます。
N' ' で括られた日本語を含む文字列リテラルは暗黙に nvarchar として扱われ、期待通りに動作します。一方、' ' で括られた方は暗黙に varchar として扱われ、文字化けしているように見えます。

(2) 日本語でカラムやテーブルを定義してローを挿入する

std::cout << "確認 #2" << std::endl;以降のブロックです。

まず、日本語を含むテーブルを作成します。
そこに、日本語を含む値をプレースホルダにバインドして INSERT してます。
全件 SELECT して表示する際、affected_row() の変化にも注目してみました。

(3) nvarchar(max)型で日本語や絵文字を含むテキストを扱う

std::cout << "確認 #3" << std::endl;以降のブロックです。

生文字列リテラルとstd::stringを連結したテキストデータが期待通りに設定・再取得できる事を確認してます。
敢えて異なる方法でバインドして、それぞれ正常に処理されてます。

Ubuntu では絵文字を含めて期待通りに処理されました。
WindowsmacOS 等が混在した状態では試してません。

(4) LIKE 演算子へのパラメータと結果セットの再利用

std::cout << "確認 #4" << std::endl;以降のブロックです。

LIKE 演算子に対する日本語を含むパラメータバインドのテストが無かったので、試してみました。
結果セットを構造体の動的配列に退避して、値の再利用も試してます。

4. まとめ

とりあえず気になった点については、自分なりに納得できる結果でした。
nanodbc で用意されているテストは網羅する範囲が広く、標準のテストに追加する仕組みも用意されているので、とても助かります。

今回は日本語が絡む為、別途プロジェクトを用意しての追試です。

実際のところ、テーブル名やカラム名に日本語を採用した現場に遭遇した事はありません。それでも、クエリと結果セットに日本語が含まれる可能性が高い為、今回のような日本語を多用するケースを敢えて検証しました。

UTF-8 にしろ UTF-16 にしろ、現在の文字コードの混沌に立ち向かえる程、私は強くないですが、せめて自分の足元くらいは確認しておきたいと思ってます。

記事のリスト

ついつい熱が入った紹介になりましたが、本当に便利だと思います。

私の場合、C++ODBC を扱うコードを書くなら Qt5 を最優先にしてましたが、これなら nanodbc も十分選択肢に入ります。

UbuntuでnanodbcからSQLServerに接続する #2 - テスト編

前回の記事では Ubuntu 環境下で nanodbc の構築とテストを実施しました。

C++ODBC 接続するのに便利な nanodbc は、そのテスト範囲も広かったです。
もう少し nanodbc で用意されているテストについて掘り下げてみます。

1. テスト内容

SQL Server 用のテスト mssql_test.cpp の内容を簡単にまとめてみました。
次の表は MS 版 ODBC ドライバでのmake testの実行結果です。

# テスト項目 内 容 結果
1 test_driver 接続文字列"DRIVER"の確認 OK
2 test_affected_rows DDL/DMLを発行してresult::affected_rows()の確認 OK
3 test_batch_insert_integral int型の配列の値をまとめてINSERTした後、SELECTして確認 OK
4 test_batch_insert_string std::string型の配列の値をまとめて、varchar(60)にINSERTする OK
5 test_batch_insert_mixed int, std::string, float型の配列の値をまとめて、int, varchar(60), float に INSERTする OK
6 test_batch_insert_describe
_param
statement::describe_param()で値をまとめてINSERTする OK
7 test_blob バイト列をvarbinary(max)で格納 OK
8 test_large_blob ファイルをvarbinary(max)で格納 OK
9 test_large_blob_geometry STGeomFromText()でgeometryインスタンスを作成して格納 OK
10 test_large_blob_geometry
_with_bind_statement
STGeomFromText()でgeometryインスタンスを作成してプレースホルダを経由 OK
11 test_blob_with_varchar 文字列をvarbinary(max)で格納 OK
12 test_block_cursor_with
_nvarchar
2行INSERTした後、SELECT時にrowset_size=2として動作確認 OK
13 test_block_cursor_with
_nvarchar_and_first_row
_null
1行目がNULL値で2行INSERTした後、SELECT時にrowset_size=2として動作確認 OK
14 test_block_cursor_with
_nvarchar_and_second
_row_null
2行目がNULL値で2行INSERTした後、SELECT時にrowset_size=2として動作確認  OK
15 test_blob_retrieve_out_of
_order
"Invalid Description Index"の発生を確認
※元ネタはこちらとの事。
OK
16 test_catalog_list_catalogs catalog::list_catalogs()の確認
データベース一覧
OK
17 test_catalog_list_schemas catalog::list_schemas()の確認
スキーマ一覧
OK
18 test_catalog_columns calalog::find_columns()の確認
カラム情報
OK
19 test_catalog_primary_keys catalog::find_primary_keys()の確認
主キー情報
OK
20 test_catalog_tables catalog::find_tables()の確認
テーブル情報
OK
21 test_catalog_table
_privileges
catalog::find_table_privileges()の確認
テーブルの権限
OK
22 test_column_descriptor result::column_name(), result::column_datatype()等の確認 OK
23 test_connection
_environment
connectionオブジェクトと接続環境の情報確認 OK
24 test_dbms_info connection::dbms_name(), connection::dbms_version()の確認 OK
25 test_get_info connection::get_info<T>()の確認 OK
26 test_decimal_conversion decimal型の変換精度の確認 OK
27 test_exception 実行時例外発生の確認 OK
28 test_execute_multiple
_transaction
statement::prepare()で複数回実行
transaction付き
OK
29 test_execute_multiple statement::prepare()で複数回実行 OK
30 test_integral int型, float型, double precision型の確認
double precisionはfloat(53)のシノニム
OK
31 test_move connectionのstd::move()を確認 OK
32 test_null NULL値のバインド、NULL値判別の確認 OK
33 test_nullptr_nulls statement::bind()の第3パラメータにnullptrを指定時の動作確認 OK
34 test_result_iterator resultの各種イテレーション確認 OK
35 test_simple 接続から結果解析まで一連のテスト OK
36 test_string std::stringとvarchar型の確認 OK
37 test_string_with_nvarchar
_max
std::stringとnvarchar(max)型の確認 OK
38 test_string_with_varchar
_max
std::stringとvarchar(max)型の確認 OK
39 test_string_with_ntext std::stringとntext型の確認 OK
40 test_string_with_text std::stringとtext型の確認 OK
41 test_string_vector std::vector<std::string>で用意したパラメータをstatemet::bind_strings()でバインドする OK
42 test_batch_binary std::vector<std::vector<uint8_t>>で用意した複数行のバイト列をバインドする OK
43 test_time nanodbc::timeとtime型の確認 OK
44 test_date nanodbc::dateとdate型の確認 OK
45 test_datetime nanodbc::timestampとdatetime型の確認 OK
46 test_decimal decimal(19,4)型の精度確認 OK
47 test_money money型の精度確認 OK
48 test_datetime2 datetime2型の精度確認 OK
49 test_datetimeoffset datetimeoffset型の精度確認 Failed
50 test_statement_with
_empty_connection
statementオブジェクトに空の接続を指定した場合の動作確認 OK
51 test_transaction transaction付きの一連の動作確認 OK
52 test_while_not_end
_iteration
result::at_end()で判断してからresult::next()して処理 OK
53 test_while_next_iteration result::next()の戻り値で判断して処理 OK
- test_async Windowsのみ。 -
- test_bind_variant Windowsのみ。 -

Linux 環境での mssql_tests のテスト内容は、以上の 53 項目です。
意訳/異訳/省略してる箇所もありますが、テストの内容はこんな感じです。

2. test_datetimeoffset の失敗について

ソースコードを読むと、現在の nanodbc の仕様では、datetimeoffset 型の値は SQL_C_SS_TIMESTAMPOFFSET ではなく、SQL_C_TIMESTAMP として扱うようです。

その際、タイムゾーンに基づいたローカルタイムへの変換を引き起こす、との事。

テストでは datetimeoffset 型に '2006-12-30T13:45:12.345-08:00' をINSERTします。
この値は、日本のタイムゾーンに従って変換されると、日付が変わってしまいます。

・datetimeoffset_test.sql :

SET NOCOUNT ON
DECLARE @d as datetimeoffset = '2006-12-30T13:45:12.345-08:00'
DECLARE @a table (tz varchar(3), dt datetimeoffset)
INSERT INTO @a VALUES ('', @d)
INSERT INTO @a VALUES ('PST', @d AT TIME ZONE 'Pacific Standard Time')
INSERT INTO @a VALUES ('UTC', @d AT TIME ZONE 'UTC')
INSERT INTO @a VALUES ('JST', @d AT TIME ZONE 'Tokyo Standard Time')
SELECT * FROM @a

このクエリの実行結果がこちらです。

$ sqlcmd -Slocalhost -Usa -Pabcd1234$ -i datetimeoffset_test.sql 
tz  dt                                           
--- ---------------------------------------------
               2006-12-30 13:45:12.3450000 -08:00
PST            2006-12-30 13:45:12.3450000 -08:00
UTC            2006-12-30 21:45:12.3450000 +00:00
JST            2006-12-31 06:45:12.3450000 +09:00

これは、nanodbc というより Time Zone / Local Time の解釈の程度問題かと。
テストを通過するには、ソースコードの当該 REQUIRE を改変するしかないです。

3. 補足 : FreeTDS の ODBC ドライバの場合

Ubuntu 18.04 LTS のパッケージ tdsodbc (1.00.82) でも試してみました。
詳細は省きますが、テストの 53 項目中 23 項目が失敗します。

試してませんが、本家 FreeTDS stable版 1.1.26 だと、結果が異なるかもしれません。

....

FreeTDS の名誉の為に書きますが、私はこの結果をあまり問題視していません。

テストの REQUIRE と実行結果の差異は、nanodbc 利用時の挙動を確認する手段でしかない、という認識です。

実際、 tdsodbc は細かい部分で、MS 版 ODBC ドライバとは挙動が異なります。

例えば、#46 test_deciaml の件では、数値型 decimal(19,4) を受ける際、64bit 固定小数で扱える最大値・最小値を評価します。

MS 版 ODBC は文字列と同じですが、tdsodbc では数値として double で丸めた後に文字列化した値と同じになり、評価としては小数部の一部が欠落したように見えます。

C++のプリミティブ型に 64bit 固定小数型が存在しない以上、受けた値をどのように扱うかは Coder 次第かなと。 

続きはこちら

UbuntuでnanodbcからSQLServerに接続する #1 - 導入編

C++ODBC 接続するのに便利な nanodbc (http://nanodbc.io/) という C++ wrapper が MIT ライセンスで公開されています。
nanodbc は Windows だけでなく LinuxmacOS 環境にも対応しています。
私も Ubuntu に環境を用意して試してみました。

1. 環境

  • Ubuntu Desktop 18.04.4 LTS 日本語Remix (Linux Kernel 5.3.0-46-generic)
  • SQL Server 2019 (RTM-CU4) (KB4548597) - 15.0.4033.1 (X64) on Linux
  • MS 版 ODBC ドライバ version 17.5.2
  • unixODBC 2.3.7
  • g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
  • git version 2.17.1
  • cmake version 3.17.20200324-gc98ec36

次の過去記事も前提としています。

2. 準備

(1) 作業ディレクト
$ cd ~
$ mkdir work
$ cd work
(2) リポジトリのクローン

GitHub に公開されているリポジトリを取得します。

$ git clone https://github.com/nanodbc/nanodbc.git
(3) ビルド用ディレクトリの準備
$ cd nanodbc
$ mkdir build
$ cd build
$ pwd
/home/dareka/work/nanodbc/build

以降の操作は、このディレクトリで作業します。

3. ビルド

(1) Makefile 生成

CMake のオプションで共有ライブラリの構築を指定します。

$ cmake -DBUILD_SHARED_LIBS=ON ..
-- The CXX compiler identification is GNU 7.5.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ - works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- nanodbc version: 2.13.0
-- nanodbc compile: C++14
-- Performing Test CXX_SUPPORTS_STDLIB
-- Performing Test CXX_SUPPORTS_STDLIB - Failed
-- nanodbc build: Disable linking libc++ - OFF
-- nanodbc feature: ODBC Version Override - OFF
-- nanodbc feature: Disable async features - OFF
-- nanodbc feature: Enable Unicode - OFF
-- nanodbc feature: Enable Boost - OFF
-- nanodbc feature: Enable SQL_NO_DATA bug workaround - OFF
-- nanodbc build: ODBC on Unix - unixODBC
-- ODBC compile flags: -I/usr/include -DHAVE_UNISTD_H -DHAVE_PWD_H -DHAVE_SYS_TYPES_H -DHAVE_LONG_LONG -DSIZEOF_LONG_INT=8
-- ODBC link flags: 
-- nanodbc build: Enable nanodbc target - SHARED
-- nanodbc build: Disable install target - OFF
-- nanodbc build: Disable tests target - OFF
-- nanodbc build: Disable examples target - OFF
-- Configuring done
-- Generating done
-- Build files have been written to: /home/dareka/work/nanodbc/build
(2) 共有ライブラリ構築
$ make nanodbc
Scanning dependencies of target nanodbc
[ 50%] Building CXX object CMakeFiles/nanodbc.dir/nanodbc/nanodbc.cpp.o
[100%] Linking CXX shared library lib/libnanodbc.so
[100%] Built target nanodbc

後はインストールすれば完了ですが、念の為、テストを実行します。
テストが不要であれば、「5. インストール」に進みます。

4. テスト

SQL Server に接続してテストを実行します。
テスト内容の都合により、後述のテストは DSN-less で実施します。
なお、実際に nanodbc を使用する際は DSN 指定でも問題ありません。

(1) テスト実行ファイル生成

私が必要なのは mssql_tests だけですが、とりあえず対象をすべてビルドします。

$ make tests

テストを絞るなら~/work/nanodbc/test/CMakeLists.txtの44行目をset(test_list mssql)にしてから cmake すると幸せになれるかもしれません。

(2) テスト用データベース作成

nanodbc のテストでは、実行時に必要なテーブルを作成します。(多分48個)

対象は指定されたデータベースコンテキストになります。
特に指定しない場合、既定のデータベースコンテキストは master です。

データベースを汚したくなければ、テスト用データベースを用意した方が無難です。 
コマンドラインツールがインストール済みであれば、 sqlcmd コマンドでデータベース 'my_test_db' を作成します。

$ sqlcmd -Slocalhost -Usa -Pabcd1234$ -Q"IF DB_ID (N'my_test_db') IS NULL CREATE DATABASE my_test_db"

または、unixODBC の isql コマンドでも作成できます。

$ echo "IF DB_ID (N'my_test_db') IS NULL CREATE DATABASE my_test_db" | isql -b -k "DRIVER={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$"

なお、テスト中にCREATE DATABASEが存在しますが、すぐにDROP DATABASEするので流用はできません。

(3) テスト実行

SQLServer 用のテストだけ実行するように、環境変数を調整します。
接続先のデータベースには、先程作成した 'my_test_db' を指定します。

$ export NANODBC_TEST_CONNSTR_MSSQL='DRIVER={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$;Database=my_test_db'
$ export CTEST_OUTPUT_ON_FAILURE=1

続いてテストを実行します。
mssql_tests 以外の実行結果は無視します。

$ make test
Running tests...
Test project /home/dareka/work/nanodbc/build
    Start 1: utility_tests
1/6 Test #1: utility_tests ....................   Passed    0.00 sec
    Start 2: mssql_tests
2/6 Test #2: mssql_tests ......................***Failed   25.52 sec

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mssql_tests is a Catch v2.4.2 host application.
Run with -? for options

-------------------------------------------------------------------------------
test_datetimeoffset
-------------------------------------------------------------------------------
/home/dareka/work/nanodbc/test/mssql_test.cpp:765
...............................................................................

/home/dareka/work/nanodbc/test/mssql_test.cpp:792: FAILED:
  REQUIRE( t.day == 30 )
with expansion:
  31 == 30

===============================================================================
test cases:    53 |    52 passed | 1 failed
assertions: 17491 | 17490 passed | 1 failed


    Start 3: mysql_tests
〜(以下、省略)〜

mssql_test のテスト 53 項目中、失敗したテストが1つありますが、これは想定内です。

datetimeoffset 型の値が日本のタイムゾーンに従って変換される際、日付が変わる事を想定していないだけです。よって、このテストはパス済みと見なしました。

MS 版 ODBC ドライバは中々の成績です。

成績と表現しましたが、このテストはドライバの優劣を競うものではないです。
ドライバの特性を確認して、それを考慮した不具合のないコードを書くのが目的なので、想定の範囲や期待値を決定する為の一助だと思ってます。

5. インストール

共有ライブラリやヘッダ等をインストールします。

$ sudo make install
[  6%] Built target nanodbc
[ 17%] Built target mssql_tests

  〜(ざっくり省略)〜

[100%] Built target example_empty
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/lib/libnanodbc.so.2.13.0
-- Installing: /usr/local/lib/libnanodbc.so
-- Installing: /usr/local/include/nanodbc/nanodbc.h
-- Installing: /usr/local/cmake/nanodbc-config.cmake
-- Installing: /usr/local/cmake/nanodbc-config-noconfig.cmake

これで、nanodbc の共有ライブラリが使えるようになりました。

続きはこちら