Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

SQLite で日付を扱う

SQLite には日付型がない。と、我々はいつから錯覚していたのか。

日付型はないのだが、なぜか日付を操作する関数群がある。これが超絶便利。

www.sqlite.org

先に注意点だが、これらの関数は文字列を日付として扱うのだが、 T ではなく空白文字を区切り文字として利用しており、 ISO-8601 に準拠していない 。ドキュメントに記載がある通り 1 YYYY-MM-DD HH:MM:SS というフォーマットである。ミリ秒まで含める場合は、この後ろに .SSS と続く。 DB にはこの形式で入れておいたほうが、 index を効かせられて便利である。また、タイムゾーンが入っていないことにも注意が必要だ。基本的に SQLite3 は UTC と localtime を扱えるのだが、複数のタイムゾーンから値を読み書きをすることを考えれば UTC に揃えておくのが無難だろう。その辺は、以下の記事にも書かれている。

qiita.com

さて、道具があることはわかったが、これだけで本当に日付を扱えるのか。 SQLite3 の日付関数には、非常に強力な modifier という機構がある。これは、 date や datetime の文字列を返す関数に文字列の引数の形で指定をするもので、 UNIX コマンドを | でつなげて実行するような感覚で利用できる。

例えば、入力された文字列を整形する場合に、ミリ秒まで含めたい場合には以下のように subsecond modifier を噛ませる。これで出力に .SSS が付与される。ミリ秒まである形式の日付を正規化して INSERT する場合に用いることができる。

datetime(?, 'subsecond')

localtime を UTC にしたい場合には、 utc modifier を通す。日付の足し算も modifier として +1 days のように書くといい。テーブル内に UTC でデータが入っているものとして、 localtime で日付を指定して検索を行う場合には以下のようになる。正規化された日付文字列の辞書順は時系列での sort と等しいので、きちんと index も効かせることができる。

         WHERE datetime(?, 'utc') <= timestamp_column
           AND timestamp_column < datetime(?, 'utc', '+1 days')",

なんともうまくできた仕組みである。


  1. ドキュメントには ISO-8601 であると書いてあるので注意。

境川から尾根緑道を経由して城山湖へ

サークルのグループライドに参加させてもらい、境川を登って城山湖へ登るルートを走ってきた。

境川はサイクリングロードが整備されており、町田駅から走ると約 30Km という絶妙な距離を走ることができる、町田駅発だとずっと緩やかな下りが続いていることもあり、ロングライドを試してみたい方にはおすすめのルートである。自分もブロンプトンを買ったばかりのときに下ったのだが、登ったことはなかったので行ってみた次第である。

戸塚駅が集合場所で、そこから長後街道を経由して境川に入る。その後は他の仲間と合流するため、町田駅へ。境川に入ってからは、一時間も立たずして町田駅へ到着した。 30Km の道のりを一時間で行くなんて、タイムスリップでもしたのだろうか。それもそのはず、境川の終点は藤沢駅付近であり、今回境川に入った高鎌橋から藤沢駅までは 10Km あるのである。境川を経由するだけで、別に定番の初心者向けルートを逆走しようという趣旨ではなかったわけだ。土地勘がないために戸塚と藤沢の位置関係も把握できておらず、全く気が付かなかった。

町田駅からは、境川をさらに登っていく。当たり前ではあるが、町田市から北側へも境川は続いているのである。 wikipedia によると、源流は城山湖という湖の近くのようだ。これは後から気がついたのだが、 似た名前の城山ダムにはごく最近行っている 。神奈川県でヒルクライムをしようとすると、だいたいこの辺を登ることになるということだろう。

と言っても境川からは早々に離脱し、その後は尾根緑道というところに入った。なんでも 8Km 続く散歩道のようで、永遠と歩行者向けの通路が続く。自転車は禁止されていないがスピードは出すなということなので、ゆるゆると進んでいく。

途中、 パン処 東海林 へ立ち寄って、苺&クリームチーズパンを食べた。自転車に乗ることになってパン屋によることが増えたのだが、パンは屋外でも食べやすいし、いろいろな種類があって、かつ、美味い。唯一問題があるとすれば、パンで腹が膨れてしまい、その後のランチの味が半減してしまうことである。今後はいっそのこと、パン屋といい感じのベンチがありそうな場所だけを見繕ってから出発して、そこをランチにしてしまっても良い気がする。もちろん、コンビニでもパンは買えるが、せっかくの遠出のレジャーなのに、さすがにそれはちょっと味気がない。

パンの後はうどんだ。人里離れたところにあるのに超絶人気店な、 さと山 さんを訪問。パンを食べた後なので若干胃に重みを感じるが、人気店で 30 分ほど待ったので、苦しくて食べられないということはなかった。リンクを張った Yahoo! の記事の写真にもある通り、ごぼう天はすごいボリュームで、かつ、美味かった。

ここまで来たら、後は城山湖へ登るだけである。恐らく小松ルートと呼ばれているルートで、斜度はそこそこだが 150m 程度しか登らないので、ゆるゆると登っていればすぐに頂上へ到着する。

下山後は橋本駅で解散。飲み会組もあったのだが、そのまま自走して帰るとちょうどいい時間に自宅に着きそうだったので、離脱して帰路についた。帰りは境川の未走破部分を使おうと思ったのだが、工事や舗装されていない部分があり、あまり満足には走れなかった。まあ、急いでいるわけでもなく、カロリーを消費するのが目的なので、未舗装路を押し歩くのもそんなに悪くはない。

その後はそのまま恩田川上流端から恩田川に入り、いつものルートで帰路についた。

そう言えば、恩田川上流端も先月初めて行ったのであった。その時のエントリをまだ書いてなかったので、そのうちメモ代わりに書き記しておくとしよう。

ナイトライドを断行

今日は妻が出かけていたため、息子と二人で家で留守番をしていた。息子がいるので日課鶴見川に出ることもできない。昼はマクドナルドのテイクアウトで済ませ、夕飯は息子の希望でラーメンとチャーハンを食べに近所のラーメン屋へ出かけた。

息子を風呂に入れ、体重計に乗ってみたところ、現実は厳しいと言うべきか予想通りというべきか、一日でまさかの 1Kg 増。四の五の言っている場合ではない。ナイトライドを断行だ。家を飛び出し、鶴見川に向かって漕ぎ出した。

通ったことがない人にはわからないと思うが、夜のサイクリングロードは本当に真っ暗だ。公道と違って、都心部でも河川敷の道には街灯がない。反射ベスト、フロントライトを2個、リアライトを2個。これを夜に走るときの標準装備にしているが、それでも夜のサイクリングロードを走るのは怖い。歩いている人に気が付かなければ、大事故につながる。気を緩めることなく、神経を尖らせて進んでいく。

怖いのとは裏腹に、夜のサイクリングロードはとても快適だ。障害物がほとんどないし、昼間は猛威を奮っている花粉も夜は少なめである。しかも、今日は風がない。冬は寒さと戦い、ここ1~2ヶ月は風と花粉と戦っていて、最近はずっと必死の形相で死ぬ気で自転車を漕いでいたのだが、今日は久々に快適なライドであった。しかも、夜桜もちょうど満開というおまけ付きだ。

明日からはまた雨が降る。今年の桜はこれが見納めだろう。

あまりにも気分が良かったので、近所の丘の上にある公園を追加で2本登っておいた。これで、約 80m の獲得標高となる。消費カロリーは約 900 Cal であり、増えた体重も無事にほぼ元に戻った。ついでに、ほとんど意識はしていたなかったのだけど、毎週の目標にしている 200Km も、このナイトライドで達成となった。色々と大変な一週間ではあったが、終わりよければすべてよしである。

来週も天気はすぐれないようだ。体重増の恐怖と戦う日々はまだまだ続く。

Google Mapsの不具合の傾向と対策

Google Maps の調子がよくない。

まず、数ヶ月ほど前からタイムラインがバグっている。具体的に言うと、移動を正しく検出できない傾向にある。 GPS データは正しく蓄積されているため、各場所の滞在時間を正しく設定し直すことで、隠されてしまっている移動経路を修繕することは可能だが、この作業が非常に手間がかかって仕方がない。

そしてさらに、4月に入ってから androidGoogle Maps の自転車ナビが壊れた。ナビ中にスリープにしておくと、本来は、スリープ中でも音声ナビは有効のままだし、スリープ解除するとロック解除不要でナビに戻れるという大変親切な挙動であった 1 。しかし、4月になってからスリープをするとナビが異常終了するようになってしまった。異常終了してしまうので、スリープ中は音声ナビができないし、ロックを解除して再びナビ開始ボタンを押さなければナビに戻ることができない。この挙動は自転車ナビだけであり、徒歩や自動車の方は壊れていないようである。

これを直す方法は、元も子もないが、アプリをダウングレードすることである。アイコン長押しで App info に進み、右上の ... から Uninstall updates してしまえば端末にプリインストールされたバージョンまで戻ることができる。これで問題は解決した。

Play Store の Google Maps の項目で、右上の ... から Auto update を無効化するのも忘れずに。


  1. なお、この挙動は iPhone 版にはない。

Brompton x CHPT3 デイヴィッド・ミラー ライド

デイヴィッド・ミラーさんがいらっしゃるということで、 CHPT3 オーナーとしてライドに参加してきた。

prtimes.jp

お台場までは自走することにした。電車を使っても早起きすることには変わりないし、それなら走行距離を稼いだほうがいい。土曜の朝の中原街道は通勤中の自動車が多くかつ渋滞もしていないため、交通量が多くて速度帯も速く正直を言えば通りたくはないのだが、他に良い抜け道も知らないので仕方がない。命からがらに勝どき橋まで辿り着き、渡った先のコンビニで朝食を買って公園で頂く。

朝食を取っていると、ランドセルを背負った子どもたちを連れた親子連れが現れた。桜が綺麗だから写真を撮るのかな、と思ってみていると、母親がしきりに「マンションが映るように。マンションは絶対」などと言い出した。ははあ、これがタワマン族か。となると、子どもたちも小学受験を終えた後なのだろうか。本当にそれは幸せな人生なのかなと思ったりもしたが、まあ、他人のことをとやかく言う筋合いは自分にはない。

会場には集合時間の 15 分程前に着いたのだが、すでに人がたくさん集まっていた。 CHPT3 だけで 5 台持っているという方もいらっしゃっていて、このイベントの関心の高さが伺われる。 あまり知らずに一目惚れで申し込んだら当選して購入に至った 自分がこんな場にいていいのかと、ちょっと申し訳ない気持ちになる。

写真の真ん中にいる長身の後ろ姿がデイヴィッド・ミラーさんである。さすが世界有数の元プロサイクリスト、とてもかっこよい。ライドもお台場らしく、終始海を望める素敵なコースだった。

そして、お台場と言えばこれ。

映えだけのために、妙なスロープを走ったりもした。10台以上のブロンプトンがこの坂を下ったり登ったりするのは妙な光景である。

ビッグサイトでライドは終了したので、再び自走で帰ることにする。サイクルモードを当日券で覗いて行っても良かったのだが、 Google Maps の不具合を踏んでしまってそちらをなんとかしなければならなかったりと、ちょっと忙しかったので断念した。

帰りは違う道から帰ろうと思い、海岸通りを抜けて羽田空港に出るルートを選択した。首都高速1号線の真下を走る都道316号線、通常海岸通りは、驚くべきことに車道は自転車通行止めに指定されており、走ることができない。自転車は歩道を走らせるという方針の時代にルールを制定した後、見直せずに現在に至っていると思われる。とは言え、場所柄大型の車両は多いので、それを言い訳にあえて見直しを避けているとも取れる。どちらにしろ法で決まっている以上は仕方がないので、歩道をのんびりと進んで羽田空港に辿り着いた。

第三ターミナル直結の羽田エアポートガーデンはできてから1年以上が経つが、まだ入ったことがなかったので行ってみることにした。のっけからニッポン感満載で、さすがは海外旅行客目当ての施設だなと言ったところ。

施設内にはタリーズコーヒーが入っているので、そこで一休みすることにする。第三ターミナルの到着ロビーにもタリーズがあったはずなので、第三ターミナル近隣だけで2店舗があるということになる。のどが渇いていたので、チョコリスタを一気に飲み干す。

せっかくここまで来たからにはスカイブリッジを通りたかったので、自宅へは多摩川の川崎側を通って向かうことにする。この区間は未舗装路があり、道を知らないと迷子になってしまう。自分もそこまで詳しくないので、(直したばかりの) Google Maps を頼りに進む。ざっくり言えば、スカイブリッジを渡った後は 速やかに Johnson & Johnson 横の細いレンガ道の路地から多摩川沿いの道に出て、六郷橋付近までは舗装路を進める。その後は公道を通って戸手町交差点まで行けば、また多摩川沿いに復帰できる。ちょうど川崎競馬場練習馬場の辺りで、午前中であれば馬に会うこともできる。後は川崎側を登戸付近までは進むことができる。丸子橋から先は逆に東京側は舗装路がないので、羽田空港から多摩川の東京側を北上する場合は丸子橋で渡るのが良い。

家に着いて走行距離を見ると、 70Km ほどであった。最近雨が続いているので 90Km は走りたいところだったが、イベントに参加したりカフェで事務作業をしたりしていた割にはきちんと走れたほうだと言っていいだろう。

Windows 上の Rust で SQLite

Rust でちょっとしたツールを書いているのだが、読み込む JSON が巨大過ぎてデータの一部分だけを使うにしても毎回 1 分程度読み込みに時間がかかってしまうので、パース結果だけを使いやすい形で保存しておきたくなった。もちろん、 JSON を分割して保存しておくだけでも効果はあるのだが、整形し直すのであれば JSON に拘る必要もない。

現在 Windows で開発しているので、 Windows でも動くことを要件とする。もちろん、 WSL2 で動かせばいいのだけど、せっかく cross platform な Rust で開発しているのだから、それだけのために linux の世界に閉じこもるのもナンセンスに思える 1 。それと、外部ライブラリが必要となる crate も避けたい。 Windows PC は複数あるので、このツールを動かすためだけに各 PC で1台1台ライブラリを揃えるのは億劫だ。

ChatGPT に相談したところ、初めは surrealdb を勧められた。面白そうではあるのだけど、 JSON を整形して保存するだけにしてはちょっと大げさ過ぎやしないか。そう思いながらもビルドしてみたのだが、 Windows 上ではエラーとなってサンプルが動かなかったので早々に諦めた。もちろん、インストール手順に従って最初に surrealdb をインストールすれば動くのだろうが、それは今回の要件に反する。

次に勧められたのは sled で、 pure-rust だし今回の要件としては NoSQL で十分なのでコイツを使おうと思っていたのだけど、そんな矢先に Qiita でこんな記事を見つけた。

kawasin73.hatenablog.com

SQLite が使えるのであれば、 sqlite3 コマンドラインでクエリできるし、それに越したことはない。試しにビルドをしてみたが、 Windows ではエラーでビルドできないようだ。原因はここ。 std::os::unix::fs::FileExt を使っている。

github.com

幸い、 fs::FileExt には windows 用の類似のインタフェースもあるので、そちらでお茶を濁すとビルドできた。

diff --git a/src/lib.rs b/src/lib.rs
index c7c0461..6ce5991 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -32,7 +32,7 @@ use std::cell::Cell;
 use std::cell::RefCell;
 use std::fmt::Display;
 use std::fs::OpenOptions;
-use std::os::unix::fs::FileExt;
+use std::os::windows::fs::FileExt;
 use std::path::Path;
 
 use anyhow::bail;
@@ -171,7 +171,7 @@ impl Connection {
             .open(filename)
             .with_context(|| format!("failed to open file: {:?}", filename))?;
         let mut buf = [0; DATABASE_HEADER_SIZE];
-        file.read_exact_at(&mut buf, 0)?;
+        file.seek_read(&mut buf, 0)?;
         let header = DatabaseHeader::from(&buf);
         header
             .validate()
diff --git a/src/pager.rs b/src/pager.rs
index 0d7c565..fce71a0 100644
--- a/src/pager.rs
+++ b/src/pager.rs
@@ -26,7 +26,7 @@ use std::io;
 use std::num::NonZeroU32;
 use std::ops::Deref;
 use std::ops::DerefMut;
-use std::os::unix::fs::FileExt;
+use std::os::windows::fs::FileExt;
 use std::rc::Rc;
 
 use crate::header::DatabaseHeader;
@@ -224,7 +224,7 @@ impl Pager {
             let (page1, is_new) = self.cache.get_page(PAGE_ID_1);
             let mut page1 = if is_new {
                 let mut page1 = page1.borrow_mut();
-                self.file.read_exact_at(&mut page1.buf, 0)?;
+                self.file.seek_read(&mut page1.buf, 0)?;
                 page1
             } else {
                 page1.try_borrow_mut()?
@@ -238,7 +238,7 @@ impl Pager {
             let mut trunk_page = if is_new {
                 let mut trunk_page = trunk_page.borrow_mut();
                 self.file
-                    .read_exact_at(&mut trunk_page.buf, self.page_offset(first_page_id))?;
+                    .seek_read(&mut trunk_page.buf, self.page_offset(first_page_id))?;
                 trunk_page
             } else {
                 trunk_page.try_borrow_mut()?
@@ -293,7 +293,7 @@ impl Pager {
         if is_new {
             let mut raw_page = page.borrow_mut();
             self.file
-                .read_exact_at(&mut raw_page.buf, self.page_offset(page_id))?;
+                .seek_read(&mut raw_page.buf, self.page_offset(page_id))?;
         }
         let header_offset = if page_id == PAGE_ID_1 {
             DATABASE_HEADER_SIZE
@@ -327,7 +327,7 @@ impl Pager {
         let (page1, is_new) = self.cache.get_page(PAGE_ID_1);
         let mut page1 = if is_new {
             let mut page1 = page1.borrow_mut();
-            self.file.read_exact_at(&mut page1.buf, 0)?;
+            self.file.seek_read(&mut page1.buf, 0)?;
             page1
         } else {
             page1.try_borrow_mut()?
@@ -348,7 +348,7 @@ impl Pager {
             let mut trunk_page = if is_new {
                 let mut trunk_page = trunk_page.borrow_mut();
                 self.file
-                    .read_exact_at(&mut trunk_page.buf, self.page_offset(first_page_id))?;
+                    .seek_read(&mut trunk_page.buf, self.page_offset(first_page_id))?;
                 trunk_page
             } else {
                 trunk_page.try_borrow_mut()?
@@ -407,7 +407,7 @@ impl Pager {
             let raw_page = page.try_borrow()?;
             if raw_page.is_dirty {
                 self.file
-                    .write_all_at(&raw_page.buf, self.page_offset(*page_id))?;
+                    .seek_write(&raw_page.buf, self.page_offset(*page_id))?;
                 drop(raw_page);
                 page.try_borrow_mut()?.is_dirty = false;
             }
@@ -431,7 +431,7 @@ impl Pager {
             if is_new {
                 let mut page1 = page1.borrow_mut();
                 self.file
-                    .read_exact_at(&mut page1.buf, 0)
+                    .seek_read(&mut page1.buf, 0)
                     .expect("read page 1 must succeed");
             }
             let buffer = &page1.borrow().buf;
@@ -562,12 +562,12 @@ mod tests {
     #[test]
     fn test_new() {
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[0_u8; 4096], 0).unwrap();
+        file.seek_write(&[0_u8; 4096], 0).unwrap();
         let pager = Pager::new(file, 1, 4096, 4096, None, 0).unwrap();
         assert_eq!(pager.num_pages(), 1);
 
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[0_u8; 4096 * 2], 0).unwrap();
+        file.seek_write(&[0_u8; 4096 * 2], 0).unwrap();
         let pager = Pager::new(file, 2, 4096, 4096, None, 0).unwrap();
         assert_eq!(pager.num_pages(), 2);
 
@@ -583,8 +583,8 @@ mod tests {
     #[test]
     fn test_get_page() {
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[1_u8; 4096], 0).unwrap();
-        file.write_all_at(&[2_u8; 4096], 4096).unwrap();
+        file.seek_write(&[1_u8; 4096], 0).unwrap();
+        file.seek_write(&[2_u8; 4096], 4096).unwrap();
         let pager = Pager::new(file, 2, 4096, 4096, None, 0).unwrap();
 
         let page = pager.get_page(PAGE_ID_1).unwrap();
@@ -606,8 +606,8 @@ mod tests {
     #[test]
     fn test_make_page_mut() {
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[1_u8; 4096], 0).unwrap();
-        file.write_all_at(&[2_u8; 4096], 4096).unwrap();
+        file.seek_write(&[1_u8; 4096], 0).unwrap();
+        file.seek_write(&[2_u8; 4096], 4096).unwrap();
         let pager = Pager::new(file, 2, 4096, 4096, None, 0).unwrap();
 
         let page = pager.get_page(PAGE_ID_1).unwrap();
@@ -637,7 +637,7 @@ mod tests {
     #[test]
     fn test_make_page_mut_failure() {
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[1_u8; 4096], 0).unwrap();
+        file.seek_write(&[1_u8; 4096], 0).unwrap();
         let pager = Pager::new(file, 1, 4096, 4096, None, 0).unwrap();
 
         let page = pager.get_page(PAGE_ID_1).unwrap();
@@ -660,8 +660,8 @@ mod tests {
     #[test]
     fn test_swap_page_buffer() {
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[1_u8; 4096], 0).unwrap();
-        file.write_all_at(&[2_u8; 4096], 4096).unwrap();
+        file.seek_write(&[1_u8; 4096], 0).unwrap();
+        file.seek_write(&[2_u8; 4096], 4096).unwrap();
         let pager = Pager::new(file, 2, 4096, 4096, None, 0).unwrap();
         let page_id_2 = PageId::new(2).unwrap();
 
@@ -694,8 +694,8 @@ mod tests {
     #[test]
     fn test_commit() {
         let file = tempfile::NamedTempFile::new().unwrap();
-        file.as_file().write_all_at(&[1_u8; 4096], 0).unwrap();
-        file.as_file().write_all_at(&[2_u8; 4096], 4096).unwrap();
+        file.as_file().seek_write(&[1_u8; 4096], 0).unwrap();
+        file.as_file().seek_write(&[2_u8; 4096], 4096).unwrap();
         let pager = Pager::new(file.reopen().unwrap(), 2, 4096, 4096, None, 0).unwrap();
 
         let page = pager.get_page(PAGE_ID_1).unwrap();
@@ -712,7 +712,7 @@ mod tests {
         pager.commit().unwrap();
 
         let mut buf = [0; 4096 * 2];
-        file.as_file().read_exact_at(&mut buf, 0).unwrap();
+        file.as_file().seek_read(&mut buf, 0).unwrap();
         assert_eq!(buf[..4096], [3_u8; 4096]);
         assert_eq!(buf[4096..], [4_u8; 4096]);
     }
@@ -720,7 +720,7 @@ mod tests {
     #[test]
     fn test_commit_failure() {
         let file = tempfile::tempfile().unwrap();
-        file.write_all_at(&[1_u8; 4096], 0).unwrap();
+        file.seek_write(&[1_u8; 4096], 0).unwrap();
         let pager = Pager::new(file, 1, 4096, 4096, None, 0).unwrap();
 
         let page = pager.get_page(PAGE_ID_1).unwrap();
@@ -742,7 +742,7 @@ mod tests {
             DatabaseHeaderMut::from((&mut original[..DATABASE_HEADER_SIZE]).try_into().unwrap());
         header.set_first_freelist_trunk_page_id(PageId::new(2));
         header.set_n_freelist_pages(1);
-        file.as_file().write_all_at(&original, 0).unwrap();
+        file.as_file().seek_write(&original, 0).unwrap();
         let pager = Pager::new(file.reopen().unwrap(), 1, 4096, 4096, None, 0).unwrap();
 
         let page = pager.get_page(PAGE_ID_1).unwrap();
@@ -759,7 +759,7 @@ mod tests {
             original.as_slice()
         );
         let mut buf = [0; 4096];
-        file.as_file().read_exact_at(&mut buf, 0).unwrap();
+        file.as_file().seek_read(&mut buf, 0).unwrap();
         assert_eq!(buf, original);
 
         assert_eq!(pager.first_freelist_trunk_page_id.get(), PageId::new(2));
@@ -808,7 +808,7 @@ mod tests {
         assert_eq!(file.as_file().metadata().unwrap().len(), 4096 * 2);
 
         let mut buf = [0; 4096 * 2];
-        file.as_file().read_exact_at(&mut buf, 0).unwrap();
+        file.as_file().seek_read(&mut buf, 0).unwrap();
         assert_eq!(buf[..4096], [10_u8; 4096]);
         assert_eq!(buf[4096..], [11_u8; 4096]);
     }
@@ -816,8 +816,8 @@ mod tests {
     #[test]
     fn test_allocate_page_non_empty_file() {
         let file = tempfile::NamedTempFile::new().unwrap();
-        file.as_file().write_all_at(&[1_u8; 4096], 0).unwrap();
-        file.as_file().write_all_at(&[2_u8; 4096], 4096).unwrap();
+        file.as_file().seek_write(&[1_u8; 4096], 0).unwrap();
+        file.as_file().seek_write(&[2_u8; 4096], 4096).unwrap();
         let pager = Pager::new(file.reopen().unwrap(), 2, 4096, 4096, None, 0).unwrap();
 
         let (page_id, page) = pager.allocate_page().unwrap();
@@ -834,7 +834,7 @@ mod tests {
         assert_eq!(file.as_file().metadata().unwrap().len(), 4096 * 3);
 
         let mut buf = [0; 4096 * 3];
-        file.as_file().read_exact_at(&mut buf, 0).unwrap();
+        file.as_file().seek_read(&mut buf, 0).unwrap();
         assert_eq!(buf[..4096], [1_u8; 4096]);
         assert_eq!(buf[4096..4096 * 2], [2_u8; 4096]);
         assert_eq!(buf[4096 * 2..], [3_u8; 4096]);
@@ -844,7 +844,7 @@ mod tests {
     fn test_allocate_page_failure() {
         let file = tempfile::NamedTempFile::new().unwrap();
         file.as_file()
-            .write_all_at(&[1; 4096], (MAX_PAGE_ID as u64 - 1) * 4096)
+            .seek_write(&[1; 4096], (MAX_PAGE_ID as u64 - 1) * 4096)
             .unwrap();
         let pager = Pager::new(file.reopen().unwrap(), MAX_PAGE_ID, 4096, 4096, None, 0).unwrap();
         assert_eq!(pager.num_pages(), MAX_PAGE_ID);
@@ -858,8 +858,8 @@ mod tests {
     #[test]
     fn test_allocate_page_abort() {
         let file = tempfile::NamedTempFile::new().unwrap();
-        file.as_file().write_all_at(&[1_u8; 4096], 0).unwrap();
-        file.as_file().write_all_at(&[2_u8; 4096], 4096).unwrap();
+        file.as_file().seek_write(&[1_u8; 4096], 0).unwrap();
+        file.as_file().seek_write(&[2_u8; 4096], 4096).unwrap();
         let pager = Pager::new(file.reopen().unwrap(), 2, 4096, 4096, None, 0).unwrap();
 
         let (page_id, page) = pager.allocate_page().unwrap();
@@ -881,7 +881,7 @@ mod tests {
         assert_eq!(file.as_file().metadata().unwrap().len(), 4096 * 2);
 
         let mut buf = [0; 4096 * 2];
-        file.as_file().read_exact_at(&mut buf, 0).unwrap();
+        file.as_file().seek_read(&mut buf, 0).unwrap();
         assert_eq!(buf[..4096], [1_u8; 4096]);
         assert_eq!(buf[4096..4096 * 2], [2_u8; 4096]);
     }
@@ -905,14 +905,14 @@ mod tests {
         );
         header.set_first_freelist_trunk_page_id(PageId::new(2));
         header.set_n_freelist_pages(5);
-        file.as_file().write_all_at(&header_buf, 0).unwrap();
+        file.as_file().seek_write(&header_buf, 0).unwrap();
         file.as_file()
-            .write_all_at(&[0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 5], 4096)
+            .seek_write(&[0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 5], 4096)
             .unwrap();
         file.as_file()
-            .write_all_at(&[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 6], 4096 * 2)
+            .seek_write(&[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 6], 4096 * 2)
             .unwrap();
-        file.as_file().write_all_at(&[2; 4096], 4096 * 6).unwrap();
+        file.as_file().seek_write(&[2; 4096], 4096 * 6).unwrap();
         let pager = Pager::new(file.reopen().unwrap(), 7, 4096, 4096, PageId::new(2), 5).unwrap();
 
         let (page_id, _) = pager.allocate_page().unwrap();
@@ -948,12 +948,12 @@ mod tests {
         );
         header.set_first_freelist_trunk_page_id(PageId::new(2));
         header.set_n_freelist_pages(127);
-        file.as_file().write_all_at(&header_buf, 0).unwrap();
+        file.as_file().seek_write(&header_buf, 0).unwrap();
         file.as_file()
-            .write_all_at(&[0, 0, 0, 0, 0, 0, 0, 126], 512)
+            .seek_write(&[0, 0, 0, 0, 0, 0, 0, 126], 512)
             .unwrap();
         file.as_file()
-            .write_all_at(
+            .seek_write(
                 &[
                     0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 15,
                     0, 0, 0, 16, 0, 0, 0, 17, 0, 0, 0, 18,
@@ -961,7 +961,7 @@ mod tests {
                 1024 - 36,
             )
             .unwrap();
-        file.as_file().write_all_at(&[2; 512], 512 * 17).unwrap();
+        file.as_file().seek_write(&[2; 512], 512 * 17).unwrap();
         let pager = Pager::new(file.reopen().unwrap(), 18, 512, 512, PageId::new(2), 127).unwrap();
 
         let (page_id, _) = pager.allocate_page().unwrap();
diff --git a/src/test_utils.rs b/src/test_utils.rs
index c37d3b4..fb9e0de 100644
--- a/src/test_utils.rs
+++ b/src/test_utils.rs
@@ -14,7 +14,7 @@
 
 use std::fmt::Write;
 use std::fs::File;
-use std::os::unix::fs::FileExt;
+use std::os::windows::fs::FileExt;
 use std::path::Path;
 
 use tempfile::NamedTempFile;
@@ -47,7 +47,7 @@ pub fn create_sqlite_database(queries: &[&str]) -> NamedTempFile {
 
 pub fn create_pager(file: File) -> anyhow::Result<Pager> {
     let mut header_buf = [0_u8; DATABASE_HEADER_SIZE];
-    file.read_exact_at(&mut header_buf, 0)?;
+    file.seek_read(&mut header_buf, 0)?;
     let header = DatabaseHeader::from(&header_buf);
     Ok(Pager::new(
         file,
@@ -61,7 +61,7 @@ pub fn create_pager(file: File) -> anyhow::Result<Pager> {
 
 pub fn create_empty_pager(file_content: &[u8], pagesize: u32, usable_size: u32) -> Pager {
     let file = NamedTempFile::new().unwrap();
-    file.as_file().write_all_at(file_content, 0).unwrap();
+    file.as_file().seek_write(file_content, 0).unwrap();
     Pager::new(
         file.as_file().try_clone().unwrap(),
         file_content.len() as u32 / pagesize,
@@ -75,7 +75,7 @@ pub fn create_empty_pager(file_content: &[u8], pagesize: u32, usable_size: u32)
 
 pub fn load_btree_context(file: &File) -> anyhow::Result<BtreeContext> {
     let mut header_buf = [0_u8; DATABASE_HEADER_SIZE];
-    file.read_exact_at(&mut header_buf, 0)?;
+    file.seek_read(&mut header_buf, 0)?;
     let header = DatabaseHeader::from(&header_buf);
     Ok(BtreeContext::new(
         header.pagesize() - header.reserved() as u32,

よし、使おう、と思ったのだがこの crate 、実は DB の初期化がまだ実装されていない。単体テストが通らなくてなぜかと思っていたのだが、よく読むと rusqlite という別のクレートを使っており、こちらは sqlite3 がインストールされていないとコンパイルが通らない。

github.com

ところで、この rusqlite というのは一体どんな crate なのだろう。 crates.io を見てみると、こんな記述がある。

features = ["bundled"]

これは、 crate にバンドルされた SQLite3 を使ってコンパイルするもので、今回の要件に完全に合致する。試してみると Windows 上でもそのままビルドできた。当たり前ではあるが、 rusqlite で生成した DB ファイルは WSL2 上の sqlite3 コマンドで読み書きできる。素晴らしい。

余談が 2 つある。まず、 rusqlite だが、この crate は libsqlite3-sys を使っている( github の同一リポジトリに存在する)のだが、 rusqlite のダウンロード数が 15M なのに対し、 libsqlite3-sys が 25M と大きく数がかけ離れている。これは libsqlite3-sys が他のクレートからも直接依存されているからで、 SQLxdiesel からの依存がある。それだけ信頼性のある crate ということだろう。

2 つ目の余談。知らなかったのだが、 SQLite3 はデフォルトでは LIKE の前方一致検索で index を使わない。これはデフォルトでは case insensitive なマッチを行うためで、

PRAGMA case_sensitive_like=ON;

とするだけで index が使われるようになる。一見、全然関連性のない設定なので、裏事情まで知らないと意味がわからない挙動の変化に思える。が、この話は 2006年のblog で見られるような内容なので、お前は今さら何を言っているんだ的な話なのであろう。


  1. 一方で、例えば HaskellPerl の主戦場は linux なわけで、そういうときは WSL2 に籠もれば良い。

相模湖と大垂水峠

いつものように朝 7 時の開店を待って、 モスバーガー和泉多摩川駅前店 に入店する。開店時間と場所がちょうどよいので、多摩川を上流に向かうときはいつもここで朝食を摂る。

先週は天候が悪くてほとんど自転車に乗れず、体重も 3Kg ほど増えてしまった。今日は曇りだが、雨は夜まで降らないようだ。この貴重な雨の隙間を逃すわけにはいかない。

今日は相模湖へ行く予定だ。いつもは素通りする府中四谷橋を渡って、野猿街道を八王子方面へ向かう。

野猿街道を進んでいくと、なにもないところに突然スターバックスが現れる。ここも目的地の1つだったのだが、朝食から時間があまり空いておらず、スイーツは食べられなさそうだったのでコーヒーだけ頂く。

まだまだ目的地は遠いので、引き続き無心で進んでいく。途中、曲がり角で見つけた樹木の案内に「八王子市」と書かれており、八王子市に入っていたことを知る。木は「トウカエデ」と言うらしいが、楓の季節でもないし、特に興味もなかった。

Google のナビに任せると、よくとんでもない道を通される。今日はただならぬ雰囲気の小さな陸橋に案内された。

さらに進んでいくと、いきなり城山ダムという看板に出くわした。全然意識していなかったのだが、津久井湖というところがあるらしい。

津久井湖観光センターなどというものもあったので、トイレ休憩。

山道が始まったように見えたので、この先何もなくなることを覚悟したのだが、そんなことはなくわりと建物が多い道路が続いた。近くに鉄道路線こそ無いものの、国道 413 号線沿いはそれなりに栄えているように見える。コンビニもファミレスもあり、補給に困ることもなさそうだった。

さらにしばらく進んで、ようやく最初の目的地の相模湖に到着した。

まだ何も食べていなかったので、湖畔にあった「ビーフカレーの店」を掲げているお店で昼食。入店して真ん中にあるテーブルが物置と化しており、やっているか不安になったのだがやっているとのこと。しばらく待っていると、「チーン」という音の後にカレーが現れた。空腹だったのでとても美味しく頂けたのだが、どうみてもレトルトカレーであった。そのせいで、通常サイズと大サイズがあるのだが、大サイズを頼んでもご飯が増えるだけで、カレールーの量は足りなくなってしまう。

数年前のレビューにはパスタなど他のメニューもあったので、店主が亡くなったなどの特殊な事情があるのではないかと勘ぐってしまう。

カレーを完食した後ほどなくして、相模湖駅に到着した。

ここからは本当の目的地、大垂水峠に向かって国道 20 号線を進む。・・・つもりだったのだが、 Google 先生は国道 20 号を外れて進めと仰っている。明らかに無駄な激坂に案内されているが、まあ、今日の目的はダイエットなので、それもいいだろう。言われるがままに激坂を登って、また激坂を降りて 20 号に戻って来るなどした。

肝心の国道 20 号線の方は、激坂好きな Google さんを他所に、斜度がゆるい終始平和な峠である。前評判も聞いていて知っていたので、のんびりと登っていたら、すぐに頂上にたどり着いた。

後はダウンヒルをずーっと行けば、すぐに高尾山口駅である。つい先日、会社の野外活動部の有志が高尾山を登りに来ており、無性に行きたくなったのだ。もっとも、自転車で高尾山を直接登ることはできないのだが。

これで目的はすべて達成したので、後は輪行して家に帰るだけだ。自走しても良かったのだが、あいにく夕方以降は雨の予報なので、早めに帰る必要がある。

順当に帰るとすれば、京王線高尾駅から乗って分倍河原駅で JR に乗り換えることになるが、輪行状態で乗り換えは面倒だ。多少時間もあるので、南武線まで自走することにする。地図で調べて、谷保(やほ)駅が一番近い南武線の駅のようだったので、そこを目指す。また初見では読めない駅だ。

ひたすら国道 20 号線を下っていくと、小さな橋に辿り着いた。多摩川も上流ではこんなささやかな橋で済むのかと、ちょっと感動する。

が、その感動もつかの間。また大きな橋が現れた。どうやら先程の橋は大和田橋と言って、浅川という多摩川の支流を渡す橋のようだ。通りで小さいと思った。

今度こそ、多摩川を渡る石田大橋を超えれば、目的の谷保駅はすぐそこだ。いつものように駅の看板の前で撮影して、そのままブロンプトンを畳んで南武線に乗り込んだ。