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 でこんな記事を見つけた。
SQLite が使えるのであれば、 sqlite3
コマンドラインでクエリできるし、それに越したことはない。試しにビルドをしてみたが、 Windows ではエラーでビルドできないようだ。原因はここ。 std::os::unix::fs::FileExt
を使っている。
幸い、 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 がインストールされていないとコンパイルが通らない。
ところで、この 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
が他のクレートからも直接依存されているからで、 SQLx や diesel からの依存がある。それだけ信頼性のある crate ということだろう。
2 つ目の余談。知らなかったのだが、 SQLite3 はデフォルトでは LIKE
の前方一致検索で index を使わない。これはデフォルトでは case insensitive なマッチを行うためで、
PRAGMA case_sensitive_like=ON;
とするだけで index が使われるようになる。一見、全然関連性のない設定なので、裏事情まで知らないと意味がわからない挙動の変化に思える。が、この話は 2006年のblog で見られるような内容なので、お前は今さら何を言っているんだ的な話なのであろう。