刹那(せつな)の瞬き

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

ぷららのメールを旧標準仕様のまま変更しなかったら送受信できなくなった

macOS Monterey にアップグレード後、何故か plala のメールのみ送受信できていない事に気づきました。

私が利用してるメーラーThunderbird 91 ですが、Gmail@nifty は正常です。

ただ、用途は plala からの通知を受信する程度なので、しばらく放置してました。

....

時間が取れたので、アカウント設定を見直したところ、設定値が ADSL 時代というかセキュリティを考慮していない普通の SMTP(25), POP3(110) のままでした。

セキュリティ強化が原因であれば、それは望むべきもの。
とりあえず SSL を利用するように変更したのですが、サーバ接続を拒否されます。

これはおかしいと、メールの仕様を調べたところ、plala には現在の「標準仕様」と「旧標準仕様」の二種類あるとの事。

私の契約は 2009-08-04 以前なので「旧標準仕様」のままでした。
SMTP(25), POP3(110) にしか対応していないのであれば、前述の結果にも納得です。

メールの標準仕様への移行

旧標準仕様はセキュリティ的に問題があるので、速やかに標準仕様へ移行します。

移行は簡単で、前述のリンク先の下部にある「移行について」-「移行する」ボタンをクリックします。

移行手続きの注意事項をよく読み、指示に従って操作すれば移行できます。
私は 受信容量:2GB・保存期間:無制限 を選択しました。

移行完了後、Thunderbird のアカウント設定を修正します。
※下記のサイトの step 05 の設定値がわかりやすいです。

SMTP, POP3 のサーバのホスト名を変更し、ポート番号, SSL, 認証方式を合わせます。その後、メールの送受信時にパスワードを保存して設定完了です。

メールの送受信は成功!だが、しかし...

ぷららのメールを標準仕様に移行する事で、無事にメール送受信が可能になりました。

早速、ぷららから届いたメールを確認すると、二通届いてました。

ぷららメール標準機能変更手続き完了のお知らせ
・【重要:ぷららからのお知らせ】ドコモとの合併に伴う、サービス提供事業者変更のお知らせ

んんん?
ドコモとの合併?何それ知らない!

慌ててメールを開いて、リンク先をクリックしたらリンク切れでした。

....

改めて公式サイトで内容を確認してみました。

数日後の 2022-07-01 からサービス提供事業者がドコモになるそうです。

ざっと読んだところ、私の用途では問題なさそうです。
ひかり TV は観てないし、引越しても大丈夫っぽいし。

当面 unbundle な ISP 接続サービスが継続利用できれば十分なので、この契約が維持できるなら一安心です。

macOS Monterey 12.4に移行しても周辺機器は問題なかった

先日、M1 Mac minimacOS Monterey 12.4 にアップグレードしました。

最も懸念してた Thunderbolt2 拡張ドック CalDigit Thunderbolt Station 2 は接続デバイスと共に正常に動作しています。

また、私の環境だけかもしれませんが、macOS Big Sur では Time Machine のクリーンアップ完了後、デバイスへのアクセスが完了しない現象が発生してました。

macOS Monterey 移行後、今のところ特に問題ありません。
Mojave の頃と同様に違和感なく Time Machine でバックアップを取れてます。

ようやく従来の環境に戻せた感じです。

経緯とその他諸々

macOS Monterey のリリース直後、ネット上で Bluetooth や Thunderbolt / USB-C 関連の不具合報告をいくつか見つけてしまい、アップグレードを躊躇してました。

私は変換アダプタ経由で Thunderbolt2 Dock と周辺機器を利用していますが、この組み合わせが動作不能になると非常に困ります。

アップグレードする前に、公式サポートの情報を調べてみたところ、最新の macOS Monterey 12.4 までには、懸念してた問題点は解消してるようです。

support.apple.com

....

OS のアップグレード作業では、必要最小限のハード構成で臨むのがセオリーです。

特に Thunderbolt2 拡張ドックは非 Apple 純正かつ変換アダプタ経由なので、どんな不具合に遭遇するか想像つきません。

今回のアップグレードでも、Mac mini に接続するのは USB キーボード, モニタ用の HDMI ケーブル, LAN ケーブルの 3点に絞りました。

結果、アップグレード作業はサクサク進み、特筆する事は何もありません。
ハード構成を元にして、あっさり完了です。

疑心暗鬼になりすぎたかな。

....

Apple 純正品ではないデバイスは、OS をアップグレードするまで正常に動作するか確証を持てないので、いつも不安です。

先週、WWDC22 で macOS Ventura の発表と同時に、2017 年以前の機種は全てビンテージ入り予定となりました。

既に Thunderbolt 2 ポートを持つ機種はなく、殆どのユーザには関係ないので、今後 OS のサポートが打ち切られる可能性もゼロではありません。

個人的に、現行機種はどれも各インタフェースのポート数が少なく感じます。
増やして欲しいところですが、こればかりは Apple の美学次第ですし。

切られる覚悟はできてるので、その際は古い周辺機器の処分タイミングとして、断捨離しようかと思ってます。

HDDを廃棄する前にとりあえずshredしておいた

最近、立て続けに HDD の不具合が発生しました。それも 3 ドライブも。

それぞれ異なる不具合が発生しているので、PC 自体の問題ではないはず。
「疑わしい機器は使用しない」をモットーに、これらの HDD は廃棄決定です。

私は今まで HDD を廃棄する前に dd コマンドで /dev/zero を書き込んでました。
書き込む途中で止まればハード障害、書き込めたらワンチャン使えるかなと。

しかし、つい最近 HDD の完全削除には shred コマンドが利用できる事を知ったので、廃棄する前に各 HDD を shred してみました。

1台目: HGST製 3.5インチ HDD (2010年製造)

この HDD は過去に不具合を発生した事があり、それを承知で使用してました。

Linux distribution のお試しが目的なので、多少の不具合は構いません。

Ubuntu 22.04 LTS をインストールして、しばらく遊べてたのですが、突然 write error が発生したため、使用を中止しました。

....

原因を調べるつもりはなく、shred して廃棄する事に。

対象の HDD が /dev/sdd に存在するとして、次のコマンドを実行。

$ sudo shred -v -n1 -z /dev/sdd

ランダムデータ書き込みを1巡した後にゼロ埋めをしたのですが、結果は正常終了。
てっきり write error が再発すると思ってたのに、何もないなんて...

→ うっかり再利用しないよう、このまま廃棄しました。

2台目: WD製 2.5インチ HDD (2014年製造)

1台目の HDD を廃棄して数日後、この HDD に不具合が発生しました。

この 2.5 インチ HDD は静音性に優れたものなのですが、明らかに異音がします。
シーク音とは別に、稀に金属が弾けると言うか跳ねるような音がします。

徐々にドライブ認識と OS 起動に時間が掛かるようになり、使用を中止しました。

....

割と重要なデータを保存してたので、別ドライブから起動した OS でデータ領域をコピーしたところ、稀に異音は発生しますが、無事にコピーできました。

この時点で fsck を仕掛けたのですが、結果は問題なし。
すぐに廃棄したいところですが、敢えての shred です。

異音が発生していて、いつ壊れてもおかしくないので、ゼロ埋めだけにしました。

$ sudo shred -v -z /dev/sdd

処理中はまったく異音がせず、スムーズに処理が進み、結果は正常終了。

→ 物理的な故障疑惑のある HDD は危険なので、これも廃棄しました。

3台目: WD製 3.5インチ HDD (2010年製造)

この HDD はリカバリー用にと Ubuntu 18.04 LTS を入れたまま数年放置してたのですが、1台目の HDD と同時期に Kubuntu 22.04 LTS をインストールしました。

その後、しばらくは問題なく利用できてたのですが、数日前から突然 emergency mode でループするようになりました。

ルート(/)はマウント済みだし、ディレクトリやファイルは普通に読めるのですが、reboot も shutdown もできません。

うっかり root のまま startx とタイプしてしまい、KDE が起動してしまいましたが、logout, reboot, shutdown のどれも操作できません。

....

障害切り分けの為、Ubutnu 22.04 LTS に入れ替えましたが、やはり数日後には emergency mode でループします。

全て同じ構成で HGST 製 2.5 インチ HDD に入れた Ubuntu 22.04 LTS は現在も問題なく利用できてるので、HDD 自体の不具合と判断しました。

納得できない状況ですが、検証時間がもったいないので、これも shred します。

$ sudo shred -v -n1 -z /dev/sdd

結果は正常終了。

時間が有り余ってれば badblocks やパーティションの切り分けを試すのですが、古い機器だし利用頻度も少ないので、諦めて廃棄します。

 

あとがき

古い機器とはいえ、手持ちの HDD がここまで連続して故障するのは初めてです。

HDD が主流だった頃は、1 プラッタの HDD を愛用しており、Ubuntu の LTS がリリースされたタイミングで新規購入した HDD にインストールしてました。

ある程度利用した HDD はバックアップや予備にまわして、HDD 障害を避けるように工夫してたつもりです。

....

現在、私の PC 環境では OS 起動ドライブは HDD から SSD への移行を終えてます。

HDD を利用しない事もないのですが、ほぼバックアップ用です。
今回の件もあり、バックアップ用 HDD を確認したら 2016 年製造でした。

今すぐ壊れるとは思いませんが、念の為、新しい HDD を購入して換装する予定です。

....

少し前に SSD の初期化は dd コマンドでは不十分との情報を得たので、改めて調べたところ、こちらの記事が参考になりました。

この記事で初めて shred コマンドの存在を知りました。

HDD の廃棄に shred コマンドが便利なのを知ってから一ヶ月も経過しないのに、実践する機会が訪れるとは、嬉しいのか悲しいのか。

記事には SSD の Secure Erase の手順も記載されているので、もし SSD を廃棄する機会があれば(そんな機会は嬉しくないけど)、実践してみようと思います。

実家の畑に居た見慣れない小鳥は何なんだろう

ちょうどひと月前の話です。

デスクワークをしてたら、何やら外が賑やかです。
気になってブラインドを開けると、白と黒が鮮やかな小鳥が居ました。

田舎なので、スズメ, ムクドリ, キジ, カラスはよくみますが、これは初めて(?)です。

早速、調べてみようと、PC に向かったのですが、検索キーワードを入力するところで手が止まりました。
うーん、どんなキーワードが望ましいんだろう?

しばらく考えて、まずは地域に生息する野鳥を調べてみました。

....

まずは、ついでの鳥見人のField note様のサイト内で、新潟で見られる野鳥のページを参考に当たりをつけてみます。

takibi-club.a.la9.jp

小鳥の特徴と生息域から判断すると、おそらくシジュウカラではないかと。
うーん、私の印象だとシジュウカラはもっと薄汚れてた(失礼!)ような...

 

続いて、サントリーホールディングス株式会社のサイトにあるサントリーの愛鳥活動のページを利用してみます。

www.suntory.co.jp

こちらのサイトだと、大きさ、特徴的な色、季節、環境から検索できます。

  • 大きさは「小」
  • 特徴的な色は「白系」「黒系」
  • 季節は「春」
  • 環境は「設定なし」

で、検索したところ、イラスト的にはコガラとヒガラが近いのです。
しかし生息域が合いません。

上記の条件に、環境「市街・住宅地」を加えると、コガラとヒガラは候補から外れます。環境「農耕地」にしても同様です。

 

結局、候補として残るのはシジュウカラなんですが...

シジュウカラって、グレー入ってません?

今朝、ゴミ袋を回収ステーションに持っていく途中で遭遇したのは、私の知識と記憶にあるシジュウカラなんだけどなー

もしかして、前提からして違ってる?

....

あれからひと月経過して、もうその姿にはお目にかかってません。
というか、遭遇したのはあの日だけでした。

今年は写真撮るのを忘れてたので、もし来年巡り会えたら記録に残します!

deadpool-r2d2とr2d2_odbc_apiを組み合わせたら、非同期処理でODBCからRDBを扱えた

先般、macOS, Linux 環境で Rust から odbc-api クレートを利用しました。

ここでは MS 版 ODBC ドライバに寄せて評価しましたが、odbc-api クレートはどの ODBC ドライバでも利用できる汎用的なものです。

Rust から DBMS に接続する際、専用クレートが存在しなくても ODBC ドライバがあれば解決できるかもしれません。

....

単一の ODBC 接続は評価できたので、次は接続プールです。

以降の内容は、前述した記事の環境を前提とします。
macOS, Linux 環境で試しています。

1. 接続プール r2d2 を利用して並列処理

odbc-api クレートには、接続プール r2d2 に対応した専用の r2d2_odbc_api クレートがあります。

まずは r2d2_odbc_api クレートの動作を確認してみます。

・Cargo.toml
[dependencies]
r2d2 = "0.8.9"
r2d2_odbc_api = "0.1.4"
anyhow = "1"
ソースコード: src/main.rs
use r2d2_odbc_api::{Cursor, buffers::TextRowSet};

const BATCH_SIZE: usize = 5000;
const BUFFER_SIZE: Option<usize> = Some(4096);

fn main() -> anyhow::Result<()> {
    println!("#### Start ####");
    let started = std::time::Instant::now();

    //let conn_str = "Driver={ODBC Driver 17 for SQL Server};Server=tcp:localhost,1433;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_test_db";
    let conn_str = "Driver={ODBC Driver 18 for SQL Server};Server=tcp:localhost,1833;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_test_db";
    let manager = r2d2_odbc_api::ODBCConnectionManager::new(conn_str);
    let pool = r2d2::Pool::builder().max_size(10).build(manager).unwrap();

    let mut threads = Vec::new();
    for idx in 0..20 {
        let pool = pool.clone();
        let handle = std::thread::spawn(move || {
            println!("Thread #{}", idx);
            let conn = pool.get().unwrap();
            std::thread::sleep(std::time::Duration::from_millis(200));
            let conn = conn.raw();
            let qry = "SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 誕生日 DESC";
            if let Some(cursor) = conn.execute(qry, ()).unwrap() {
                let mut buffers = TextRowSet::for_cursor(BATCH_SIZE, &cursor, BUFFER_SIZE).unwrap();
                let mut row_set_cursor = cursor.bind_buffer(&mut buffers).unwrap();
                if let Some(batch) = row_set_cursor.fetch().unwrap() {
                    for row_index in 0..batch.num_rows() {
                        let id: i32 = std::str::from_utf8(batch.at(0, row_index).unwrap_or(&[])).unwrap().parse().unwrap();
                        let name =  std::str::from_utf8(batch.at(1, row_index).unwrap_or(&[])).unwrap();
                        let birthday = std::str::from_utf8(batch.at(2, row_index).unwrap_or(&[])).unwrap();
                        println!("#{} | {} | {} | {} |", idx, id, name, birthday);
                    }
                }
            };
        });
        threads.push(handle);
    }
    for th in threads {
        let _ = th.join();
    }

    println!("#### Finish ####");
    println!("経過時間: {:?}", started.elapsed());
    Ok(())
}

接続プール数の指定や接続の取り出しは、他の r2d2 アダプタと同様に記述できます。

・実行結果

スレッド別にクエリを実行した結果が表示されるはずです。
※出力結果は、過去記事と同様なので、ここでは省略します。

....

ところで、接続プール r2d2 は async/await 構文に対応していません。
odbc-api が依存する odbc-sys も一部に注意制限事項があります。

せっかく並列処理を記述できるのに、今時の非同期ランタイムに対応していないので、使い所が難しい様に思います。

私は Rocket v0.4 で利用しましたが、状況によってはお奨めできません。

そこで試したのが、deadpool-r2d2 クレートとの併用です。

 

2. 非同期タスク対応の deadpool-r2d2r2d2_odbc_api を補助

非同期な汎用プール deadpool のアダプタに r2d2 をバックエンドとする deadpool-r2d2 があります。

理論上、deadpool / deadpool-r2d2 で環境を整えれば、r2d2 のアダプタを非同期タスク対応にできるはずなので、この方針で r2d2_odbc_api を試してみます。

・Cargo.toml
[dependencies]
deadpool = "0.9"
deadpool-r2d2 = "0.2.0"
r2d2_odbc_api = "0.1.4"
tokio = "1.18"
futures = "0.3" anyhow = "1"
ソースコード: src/main.rs
use r2d2_odbc_api::{Cursor, buffers::TextRowSet};

type ODBCManager = deadpool_r2d2::Manager<r2d2_odbc_api::ODBCConnectionManager>;
type ODBCPool = deadpool_r2d2::Pool<ODBCManager>;

const BATCH_SIZE: usize = 5000;
const BUFFER_SIZE: Option<usize> = Some(4096);

struct Member {
    id: i32,
    name: String,
    birthday: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    println!("#### Start ####");
    let started = std::time::Instant::now();

    //let conn_str = "Driver={ODBC Driver 17 for SQL Server};Server=tcp:localhost,1433;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_test_db";
    let conn_str = "Driver={ODBC Driver 18 for SQL Server};Server=tcp:localhost,1833;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_test_db";
    let manager = ODBCManager::new(
        r2d2_odbc_api::ODBCConnectionManager::new(conn_str),
        deadpool_r2d2::Runtime::Tokio1,
    );
    let pool = ODBCPool::builder(manager).max_size(10).build()?;

    let mut tasks = Vec::new();
    for idx in 0..20 {
        let pool = pool.clone();
        let handle = tokio::spawn(async move {
            println!("Async Task #{}", idx);
            let client = pool.get().await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
            let result = client
                .interact(|client| {
                    let mut records = Vec::<Member>::new();
                    let conn = client.raw();
                    let qry = "SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 誕生日 DESC";
                    if let Some(cursor) = conn.execute(qry, ()).unwrap() {
                        let mut buffers = TextRowSet::for_cursor(BATCH_SIZE, &cursor, BUFFER_SIZE).unwrap();
                        let mut row_set_cursor = cursor.bind_buffer(&mut buffers).unwrap();
                        if let Some(batch) = row_set_cursor.fetch().unwrap() {
                            for row_index in 0..batch.num_rows() {
                                records.push(Member {
                                    id: std::str::from_utf8(batch.at(0, row_index).unwrap_or(&[])).unwrap().parse().unwrap(),
                                    name: std::str::from_utf8(batch.at(1, row_index).unwrap_or(&[])).unwrap().into(),
                                    birthday: std::str::from_utf8(batch.at(2, row_index).unwrap_or(&[])).unwrap().into(),
                                });
                            }
                        }
                    };
                    records
                })
                .await
                .unwrap();
            for row in &result {
                println!("#{} | {} | {} | {} |", idx, row.id, row.name, row.birthday);
            }
        });
        tasks.push(handle);
    }
    futures::future::join_all(tasks).await;

    println!("#### Finish ####");
    println!("経過時間: {:?}", started.elapsed());
    Ok(())
}

このソースコードtokio ランタイム用ですが、async-std でも同様に記述できます。

....

deadpool-r2d2 では、接続プールから接続を取り出す際、.interact() に渡すクロージャ内で同期的処理が完結するように記述します。

理想を言えば ODBCSQL メソッド毎に await したいのですが、これはできません。

非同期タスクに対応していないものを、むやみに非同期化しても、どこかで綻びが生じるので「 deadpool-r2d2 内部の SyncGuard が保護する範囲内で ODBC に関する処理を済ませ、その結果を返却するもの」として、割り切ってます。

ソースコード中では、結果セット用の構造体を用意して、クエリの取得結果をロー毎に Vec<T> へ格納し、戻り値としています。

 

・実行結果

非同期タスク別に実行された結果が表示されます。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/tokio_odbcapi`
#### Start ####
Async Task #0
Async Task #1
Async Task #2

・・・(ざっくり省略)・・・
#19 | 105 | 江口 美奈 | 1979-06-23 | #19 | 210 | 荒井 伸次郎 | 1974-01-30 | #### Finish #### 経過時間: 746.914333ms

 

3. 所感

環境を整えるだけで、無事に非同期タスクで処理できました。

試した ODBC ドライバは SQLServer (msodbc, tdsodbc) と PostgreSQL だけですが、他の ODBC ドライバでも利用できると思います。

ここまで手軽に ODBC 接続を非同期ランタイムで利用できるとは思いませんでした。

....

元々、deadpool は tiberius の接続プールに利用したくて調査したものです。

その際、deadpool-r2d2 の存在を知り、ずっと気になってました。

今回の結果を踏まえて、当時試作した rweb (tokio) と tide (async-std) のプロジェクトを修正してみましたが、deadpool-r2d2 / r2d2_odbc_api で問題なく動作しています。

....

なお、deadpool はアダプタが豊富なので、PostgreSQL, Sqlite 等については専用クレート、MySQL は deadpool-r2d2 / r2d2_mysql の組み合わせ等があります。

目的の DBMS が単一の場合、専用クレートや専用ライブラリ・ツールに優位性があるので、積極的に ODBC ドライバを利用する機会は少ないかもしれません。

それでも、非同期タスクで異なる DBMS を扱える deadpool-r2d2 / r2d2_odbc_api / ODBC ドライバの組み合わせは便利だと思います。

odbc-apiクレートに乗り換えたら、MS版ODBCドライバからSQLServerへの接続が色々と捗った

macOS, Linux 環境での話です。

先月 MS 版 ODBC ドライバが更新されたのを機に、改めて Rust で ODBC ドライバを扱うクレートを探したところ、odbc-api クレートを発見しました。

後述する内容は、私なりに調べた結果をまとめたものです。

※追記: 2022-05-29 前提が抜けてました

この記事は、既存環境との互換性を考慮して Unicode データの格納先を nvarchar 項目とし、 SQL Server 2019 でデータベースファイル作成時の照合順序に _UTF8 オプションを指定しなかった場合の顛末です。

例えば、CREATE DATABASE my_test_db COLLATE Japanese_XJIS_140_CI_AS_UTF8; とすれば、varchar 項目が UTF-8 で符号化されたデータの格納先になるので、項目パラメータはそのまま渡せます。

odbc-api クレートについて

最新の MS 版 ODBC ドライバと組み合わせて、SQL Server への接続を試したところ、私が ODBC 接続で処理したい範囲については、ほぼ網羅されてました。

概ね問題なく利用できたのですが、現状 odbc-api クレートと MS 版 ODBC ドライバの組み合わせでは、いくつか注意が必要です。

  1. プレースホルダ経由で nvarchar 項目に文字列を渡す際、Unicode 補助文字を含む文字列を UTF-8 のまま渡すと補助文字が ?? に変換される。
  2. varchar(max), nvarchar(max), varbinary(max) はバッファリングの仕様上、そのまま値を取得できない。SQL 文でサイズを固定するか、BufferDescription を記述する必要がある。
  3. Text 系, Blob 系は、非推奨な text, ntext, imageにマップされるので、データベースの照合順序によっては利用不可。

私が利用する範囲で困ったのはこの 3 点です。
逆に言えば、この 3 点以外はまったく困りませんでした。とても便利です。

....

上記の項番 2, 3 については、使用頻度が稀なので、制約を考慮して個別対応しました。
項番 1 については回避策があるので、検証用コードで確認してみます。

・検証用コード (main.rs)

文字列'𠮷原 🍑子' は 𠮷 (つちよし: U+20BB7) と絵文字の桃 (U+1F351) を含みます。
この文字列を 4 つのパターンでテーブルに INSERT します。

use odbc_api::IntoParameter;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let environment = odbc_api::Environment::new()?;
    let conn_str = "Driver={ODBC Driver 18 for SQL Server};Server=tcp:localhost,1833;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_test_db";
    let conn = environment.connect_with_connection_string(conn_str)?;

    conn.execute("DROP TABLE IF EXISTS ins_test", ())?;
    conn.execute("CREATE TABLE ins_test (id int, name nvarchar(40))", ())?;

    // 文字列リテラル
    conn.execute("INSERT INTO ins_test (id, name) VALUES (111, '𠮷原 🍑子')", ())?;
    conn.execute("INSERT INTO ins_test (id, name) VALUES (222, N'𠮷原 🍑子')", ())?;

    // プレースホルダ経由
    let mut prepared = conn.prepare("INSERT INTO ins_test (id, name) VALUES (?,?)")?;
    let name = "𠮷原 🍑子"; 

    // &str    
    prepared.execute(( &333_i32, &name.into_parameter() ))?; 

    // &str -> UTF-16LE -> &[u8]
    let name_utf16 = utf16string::WString::<utf16string::LE>::from(name);
    let name_bin = name_utf16.as_bytes();
    prepared.execute(( &444_i32, &name_bin.into_parameter() ))?; 
    Ok(())
}
・実行結果の確認

コード実行後、テーブルの内容を Azure Data Studio で確認した画面がこちらです。

Unicode 文字列を含むクエリは、その文字列に N プレフィックスをつける必要があるので、N 無しは文字化け、N 有りは問題なく処理されます。
どちらも想定通りです。

一方、プレースホルダを経由する場合、nvarchar 項目 (UTF-16LE) に対し、UTF-8 の &str をそのままパラメータに渡すと、Unicode 補助文字がすべて??(0x003F, 0x003F) に置き換わります。※バイトオーダーでは (3F 00 3F 00)

これは意図した結果ではありません。

プレースホルダを経由しなければ気にする必要はないのですが、スマートに処理できないのは困ります。

....

回避策のひとつに、UTF-8 を UTF-16LE に変換して、バイトオーダーを保ったまま、そのバイト列 &[u8] をパラメータに渡す方法があるのですが、これが少々面倒です。

odbc-api クレートには widestring クレート由来のodbc_api::U16Stringが用意されていますが、これはワイド文字単位に処理するものです。
そもそも用途が異なるのでバイト列に変換するメソッドは実装されていません。

調べてみたところ、utf16string クレートが条件を満たしてました。
エンディアンを考慮し、内部バッファをバイト列に変換するメソッド as_bytes() が実装されていたので、今回はこちらを採用しました。

結果、バイト列 &[u8] は odbc-api では varbinary のパラメータとして処理され、無事にプレースホルダ経由で Unicode 文字列を渡せています。

※この仕様が変更される可能性はありますが、現在はこの方法で対処可能です。

CRUD なコードを試してみた

前述の内容を踏まえて、Unicode の検証ついでに CRUD なコードを書いてみました。

まずはcargo new等で用意したプロジェクトを修正します。
私はcargo new crud_utf16としました。

・Cargo.toml の編集

[dependencies]
odbc-api = "0.36.1"
utf16string = "0.2"
anyhow = "1"

ソースコード: src/main.rs

接続したデータベースに「会員名簿」テーブルを作成します。
ODBC ドライバの接続文字列 conn_str は試す環境に合わせて変更してください。

use odbc_api::{Cursor, IntoParameter, ResultSetMetadata};

mod msodbc;

fn main() -> anyhow::Result<()> {
    println!("#### Start ####");

    let environment = odbc_api::Environment::new()?;
    //let conn_str = "Driver={ODBC Driver 17 for SQL Server};Server=tcp:localhost,1433;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_test_db";
    let conn_str = "Driver={ODBC Driver 18 for SQL Server};Server=tcp:localhost,1833;TrustServerCertificate=yes;UID=sa;PWD=abcd1234$;Database=my_xjis140_db";
    let conn = environment.connect_with_connection_string(conn_str)?;

    println!("-- DROP & CREATE TABLE");
    conn.execute("DROP TABLE IF EXISTS 会員名簿", ())?;
    conn.execute("CREATE TABLE 会員名簿 (番号 int, 氏名 nvarchar(40), 誕生日 date)", ())?;

    println!("-- INSERT");
    let mut prepared = conn
        .prepare("INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)")
        .unwrap();
    let members = [
        (110, "岸本 龍也", "1989-11-06"),
        (210, "荒井 伸次郎", "1974-01-30"),
        (105, "江口 美奈", "1979-06-23"),
        (222, "渡辺 蹴斗", "2002-05-29"),
        (304, "長田 隆次", "1991-05-25"),
        (444, "𠮷原 🍑子", "2002-05-05"), // 吉,桃 ではなく つちよし,絵文字
    ];
    for (id, name, birthday) in members {
        let params = (
            &(id as i32),
            &msodbc::NVarChar::from(name).into_parameter(),
            &birthday.into_parameter(),
        );
        prepared.execute(params).unwrap();
    }
    print_resultset(&conn, "SELECT * FROM 会員名簿 ORDER BY 番号")?;

    println!("-- UPDATE");
    let mut prepared = conn
        .prepare("UPDATE 会員名簿 SET 氏名=? WHERE 番号=?")
        .unwrap();
    let new_name = msodbc::NVarChar::from("渡\u{908A}\u{E0104} 蹴斗⚽");   // 邊の異体字, 絵文字
    let params = (
        &new_name.into_parameter(), 
        &222_i32,
    );
    prepared.execute(params).unwrap();
    print_resultset(&conn, "SELECT * FROM 会員名簿 ORDER BY 番号")?;

    println!("-- DELETE");
    let mut prepared = conn.prepare("DELETE FROM 会員名簿 WHERE 番号=?").unwrap();
    let delete_id: i32 = 210;
    prepared.execute(&delete_id).unwrap();
    print_resultset(&conn, "SELECT * FROM 会員名簿 ORDER BY 番号")?;

    println!("#### Finish ####");
    Ok(())
}

// テーブル内容の表示
fn print_resultset(cn: &odbc_api::Connection, qry: &str) -> anyhow::Result<()> {
    if let Some(cursor) = cn.execute(qry, ()).unwrap() {
        // カラム名表示
        let headline: Vec<String> = cursor.column_names()?.collect::<Result<_, _>>()?;
        for col_name in headline {
            print!(" | {}", col_name);
        }
        println!(" |");
        // 取得ロー表示
        let mut buffers =
            odbc_api::buffers::TextRowSet::for_cursor(5000, &cursor, Some(4096)).unwrap();
        let mut row_set_cursor = cursor.bind_buffer(&mut buffers).unwrap();
        let mut count = 0;
        while let Some(batch) = row_set_cursor.fetch().unwrap() {
            let num_rows = batch.num_rows();
            for row_index in 0..num_rows {
                for col_index in 0..batch.num_cols() {
                    match batch.at(col_index, row_index) {
                        Some(val) => print!(" | {}", std::str::from_utf8(val).unwrap()),
                        None => print!(" |  (NULL) "),
                    };
                }
                println!(" |");
            }
            count += num_rows;
        }
        println!("結果 {} 行", count);
    }
    Ok(())
}

ソースコード: src/msodbc.rs

プレースホルダのパラメータ作成を補助するコードを別モジュールにしました。

一時的な用途、しかも変換結果を Vec<u8> で返す関数なら数行書けば済むところ、大袈裟すぎる感じはしますが後悔はしてません。

pub struct NVarChar {
    text: utf16string::WString<utf16string::LE>,
}

impl NVarChar {
    pub fn as_bytes(&self) -> &[u8] {
        self.text.as_bytes()
    }
}

impl From<&str> for NVarChar {
    fn from(val: &str) -> Self {
        Self {
            text: utf16string::WString::from(val),
        }
    }
}

impl odbc_api::IntoParameter for NVarChar {
    type Parameter = odbc_api::parameter::VarBinaryBox;
    fn into_parameter(self) -> Self::Parameter {
        odbc_api::parameter::VarBinaryBox::from_vec(self.text.as_bytes().to_vec())
    }
}

impl<'a> odbc_api::IntoParameter for &'a NVarChar {
    type Parameter = odbc_api::parameter::VarBinarySlice<'a>;
    fn into_parameter(self) -> Self::Parameter {
        odbc_api::parameter::VarBinarySlice::new(self.text.as_bytes())
    }
}

今回の src/main.rs では impl NVarChar のas_bytes()を使用してません。
これは BufferDescription から確保したバッファで、AnyColumnViewMut::Binary()等の処理の際、バイト列が必要になるので、意図を込めて実装してあります。

utf16string クレート完全依存なら std::ops::Deref トレイトを書くと簡潔なのですが...

環境

macOSLinux 環境で試してみました。

(1) macOS Big Sir 11.6.5 (Apple Silicon) ※ Intel も同様

ソースコードODBC 接続文字列は、 macOS の Docker で動作する Azure SQL Edge の TCP:1433 を localhostTCP:1833 にマップした環境、に対してのものです。

(2) KDE neon 5.24.4 (Ubuntu 20.04 ベース)

実行結果

cargo run で実行した結果がこちらです。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/crud_utf16`
#### Start ####
-- DROP & CREATE TABLE
-- INSERT
 | 番号 | 氏名 | 誕生日 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 210 | 荒井 伸次郎 | 1974-01-30 |
 | 222 | 渡辺 蹴斗 | 2002-05-29 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 444 | 𠮷原 🍑子 | 2002-05-05 |
結果 6 行
-- UPDATE
 | 番号 | 氏名 | 誕生日 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 210 | 荒井 伸次郎 | 1974-01-30 |
 | 222 | 渡邊󠄄 蹴斗⚽ | 2002-05-29 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 444 | 𠮷原 🍑子 | 2002-05-05 |
結果 6 行
-- DELETE
 | 番号 | 氏名 | 誕生日 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 222 | 渡邊󠄄 蹴斗⚽ | 2002-05-29 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 444 | 𠮷原 🍑子 | 2002-05-05 |
結果 5 行
#### Finish ####

無事に Unicodeサロゲート文字、異体字、絵文字を処理できてます。
macOS, Linux 環境どちらでも同じ結果になりました。

Apple Silicon でld: library not found for -lodbcが発生する場合

homebrew 経由で mssql-tools18 または msodbcsql18 をインストールすると、依存関係で unixodbc もインストールされます。

ライブラリは /opt/homebrew/lib に存在するのですが、これを見つけてくれません。
私は~/.zshrcexport RUSTFLAGS='-L /opt/homebrew/lib'を追加して対処しました。

所感

記事の内容は FreeTDS 版 ODBC ドライバでも発生しますが、同様に対処可能です。

一応 PostgreSQL でも試してみましたが、プレースホルダの問題は発生しません。
UTF-8 対応なので、varchar 項目に対し、普通にパラメータを渡すだけです。

....

2 年程前、 ODBC ドライバ経由での接続には odbc クレートを利用しました。

私は MS 版 ODBC ドライバを利用したくて調査したのですが、苦労した割に結果は思わしくありません。当時はたまたま FreeTDS 版 ODBC ドライバで回避できたのですが、いつもこの手を使えるとは限りません。

ここ最近は async/await 対応で直接 TDS プロトコルを扱う tiberius クレートばかりで、ODBC ドライバ経由で SQL Server を利用する機会はありませんでした。

Rust での ODBC に関して半ば諦めてたところ、発見したのが odbc-api クレートです。
これ幸いと試してみたら、もう完全に乗り換えても良いくらい便利でした。

特に odbc クレートと MS 版 ODBC ドライバの組み合わせで致命的な、

  • そもそも setlocale() しないと日本語を扱えない。
  • SQLDescribeCol() の NameLengthPtr は文字数を指すのに、文字サイズを考慮せずバッファを確保するため、日本語名だと欠落or文字化けする。

に関しては、何もしなくて大丈夫です。最高です!

....

並列処理については r2d2_odbc_api クレートが利用できますが、非同期ランタイムで ODBC を扱う場合は deadpool_r2d2 クレートと連携する手段もあります。

odbc-api クレートの更新は継続されているようなので、今後にも期待してます。

Node.jsのfetch()でデータを取得して、Excel 2019 for Macに渡してみる

前回、外部データ取り込みに Web クエリを利用しました。

この記事では、http リクエストに対して、ソースコード内に記述した <table> 要素をデータとして返します。しかし、いつも同じ内容では物足りないです。

実際の用途を考慮すると、Web API というか RESTful API みたいなサービスにアクセスして JSON データを得て、その結果を加工して Excel に渡したいものです。

....

その準備として、改めて Node.js で Fetch 関連のパッケージを確認していたところ、Node.js v17.5 から fetch() が試験的実装として利用できるとの情報を得ました。

fusebit.io

元々 node-fetch で対処するつもりでしたが、早速これを試したくなりました。

試してみた

実在する Web API を前提にすると関係各位に迷惑をかけそうなので、ここでは私が用意したサーバ環境で話を進めます。

(1) JSON 配列を返すサーバ

仮にhttp://example.jp/api/v1/membersにアクセスすると、データベースから名簿を抽出して JSON 配列で返却するサービスがあるとします。

実際に返される JSON 配列はこんな感じです。

[
  { "番号": 304, "氏名": "長田 隆次", "誕生日": "1991/05/25" },
  { "番号": 110, "氏名": "岸本 龍也", "誕生日": "1989/11/06" },
  { "番号": 307, "氏名": "中井 雄樹", "誕生日": "1984/02/29" },
  { "番号": 105, "氏名": "江口 美奈", "誕生日": "1979/06/23" }
]

この JSON 配列をテーブル形式に戻したレイアウトがこちらです。

番号 氏名 誕生日
304 長田 隆次 1991/05/25
110 岸本 龍也 1989/11/06
307 中井 雄樹 1984/02/29
105 江口 美奈 1979/06/23

あくまで試用なので、シンプルなものにしてみました。

このデータを Excel に渡す事が目標になります。

(2) fetch()で取得したデータを <table> 要素に加工するローカルサーバ

検証用の Node.js 環境は前回と同様です。
※使用マシン: M1 Mac mini (macOS Big Sur 11.6.5)

$ node -v
v17.8.0
  1. http リクエストを受ける。
  2. Node.js に実装された fetch() で前述のサーバにアクセス。
  3. fetch() で取得した JSON 配列を <table> 要素に加工。
  4. その結果を http レスポンスで返す。

そんなローカルサーバを用意します。

....

任意のディレクトリに次のファイルを作成します。

・ファイル名: main.js

const http = require('http');

const port = process.env.PORT || 3000;

const server = http.createServer(async (_request, response) => {
    try {
        // JSON配列を取得
        const res = await fetch("http://example.jp/api/v1/members");
        const members = await res.json();
        console.log(members);
        // ExcelのWebクエリで処理できる形式にして送信
        response.writeHead(200, {
            "Content-Type": "text/html; charset=utf-8"
        });
        const msg = await toHtmlTable(members);
        response.end(msg);
        console.log(`送ったよ: ${Date(Date.now())}`);
    } catch (error) {
        console.error(error);
    }
});
server.listen(port);
console.log(`待機中 (PORT: ${port})`);

async function toHtmlTable(json_array) {
    if (json_array.length > 0) {
        // カラム名
        const columns = Object.keys(json_array[0]);
        const thead = [];
        thead.push("<thead>");
        thead.push("<tr>");
        for (const col_name of columns) {
            thead.push(`<th>${col_name}</th>`);
        }
        thead.push("</tr>");
        thead.push("</thead>");
        // ロー
        const tbody = [];
        tbody.push("<tbody>");
        for (const row of json_array) {
            const td = [];
            for (const col_name of columns) {
                td.push(`<td>${row[col_name]}</td>`);
            }
            tbody.push("<tr>");
            tbody.push(td.join("\n"));
            tbody.push("</tr>");
        }
        tbody.push("</tbody>");

        const table = [];
        table.push("<table>");
        table.push(thead.join("\n"));
        table.push(tbody.join("\n"));
        table.push("</table>");
        return table.join("\n");
    }
    return "";
}

効率が悪いコードですが、私が確認しやすい構成にしてます。

....

試験的な機能を有効にする為、パラメータ付きでローカルサーバを起動します。

$ node --experimental-fetch main.js
待機中 (PORT: 3000)
(node:6515) ExperimentalWarning: Fetch is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

警告メッセージが表示されますが気にしません。

(3) ローカルサーバが返す内容を Excel 側に取り込む

データ取得用の iqy ファイルも前回と同様です。

・ファイル名: my_web_query.iqy

WEB
1
http://localhost:3000/

Selection=1
Formatting=HTML
PreFormattedTextToColumns=True
ConsecutiveDelimitersAsOne=True
SingleBlockTextImport=True
DisableDateRecognition=False
DisableRedirections=False

この iqy ファイルを Excel 2019 for Mac のメニューバーにある「データ」-「外部データ取り込み」-「Web クエリの実行...」から表示される画面で選択し、そのまま Excel に取り込みます。

f:id:infinity_volts:20220408220247p:plain

これで目標は達成できました。

データベースが変更された場合、Excel 側でリボンの「すべて更新」をクリックすると、最新データに置き換わります。

JSON データの構造が複雑な場合もあるよ

いつもデータ構造が単一のテーブル形式とは限りません。

この例は、気象庁の天気予報サイトのレイアウトを真似しつつ、JSON データを表の枠組みに落とし込んだ結果です。

※この画像内のデータは修正を加えたものです。

f:id:infinity_volts:20220408222033p:plain

JSON データの構造に従い、colspan, rowspan 属性で表組みをして、style 属性で体裁を整えると Excel でもそれなりに表示してくれます。 

当初 <table> 要素の復習として、気象庁が公開してる JSON データを fetch() で処理するコードを書いたのですが、諸々を考慮して記事に載せるのは控えました。

 

あとがき

今のところ fetch() に関しては、特に問題なく動作してます。

私は最近、とりあえず node-fetch、用途によって SuperAgent, axios な感じでした。
パッケージとは異なり npm install 無しで使えるのが嬉しいです。

今後 fetch() の採用が中止になるとは思ってませんが、正式採用のアナウンスが来ないうちは現状維持ですね。

....

なお、この記事で試した環境であれば、わざわざ fetch() を利用せずとも、直接データベースにアクセスして後処理すれば同じ結果が得られます。

しかし、いつもそれが可能とは限りません。
外部はもちろん、 LAN 上のサービスでも、諸事情により提供されるデータ形式が選択できない場面があります。

そのため、ここでは JSON データ取得→加工→Excel の流れを実現する手段の一つとして検証してみました。

そして、Excel 2019 for Mac への Power Query の提供が待ち遠しいです。
来週くらいに更新が来るかもしれませんが、今回はどうなんでしょう。