use std::{io::Cursor, path::Path};
use rusqlite::{Connection, params};
use zstd::{decode_all, encode_all};
use crate::{
error::StorageError,
model::{Docset, FavoriteEntry, PageContent, SearchEntry},
};
const SCHEMA: &str = include_str!("../tables.sql");
fn compress_maybe_dict(
data: &[u8],
level: i32,
dict: Option<&[u8]>,
) -> Result<Vec<u8>, StorageError> {
if let Some(d) = dict {
let mut enc =
zstd::bulk::Compressor::with_dictionary(level, d).map_err(StorageError::Zstd)?;
enc.compress(data).map_err(StorageError::Zstd)
} else {
encode_all(data, level).map_err(StorageError::Zstd)
}
}
fn decompress_maybe_dict(data: &[u8], dict: Option<&[u8]>) -> Result<Vec<u8>, StorageError> {
const MAX_DECOMPRESSED: usize = 64 * 1024 * 1024;
if let Some(d) = dict {
let mut dec = zstd::bulk::Decompressor::with_dictionary(d).map_err(StorageError::Zstd)?;
dec.decompress(data, MAX_DECOMPRESSED)
.map_err(StorageError::Zstd)
} else {
decode_all(Cursor::new(data)).map_err(StorageError::Zstd)
}
}
pub struct Storage {
conn: Connection,
compression_level: i32,
}
impl Storage {
pub fn open(path: &Path, compression_level: i32) -> Result<Self, StorageError> {
let conn = Connection::open(path)?;
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
conn.execute_batch("PRAGMA journal_mode = WAL;")?;
conn.execute_batch(SCHEMA)?;
Ok(Self {
conn,
compression_level,
})
}
pub fn insert_docset(
&self,
name: &str,
version: Option<&str>,
path: Option<&str>,
) -> Result<i64, StorageError> {
self.conn.execute(
"INSERT OR IGNORE INTO docsets (name, version, path) VALUES (?1, ?2, ?3)",
params![name, version, path],
)?;
let id: i64 = self.conn.query_row(
"SELECT id FROM docsets WHERE name = ?1",
params![name],
|row| row.get(0),
)?;
Ok(id)
}
pub fn list_docsets(&self) -> Result<Vec<Docset>, StorageError> {
let mut stmt = self.conn.prepare(
"SELECT id, name, version, path, is_favorite \
FROM docsets ORDER BY is_favorite DESC, name",
)?;
let rows = stmt.query_map([], row_to_docset)?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
pub fn delete_docset(&self, id: i64) -> Result<(), StorageError> {
self.conn
.execute("DELETE FROM docsets WHERE id = ?1", params![id])?;
Ok(())
}
pub fn set_docset_dictionary(&self, docset_id: i64, dict: &[u8]) -> Result<(), StorageError> {
self.conn.execute(
"UPDATE docsets SET dictionary = ?1 WHERE id = ?2",
params![dict, docset_id],
)?;
Ok(())
}
pub fn get_docset_dictionary(&self, docset_id: i64) -> Result<Option<Vec<u8>>, StorageError> {
let mut stmt = self
.conn
.prepare("SELECT dictionary FROM docsets WHERE id = ?1")?;
let dict: Option<Vec<u8>> = stmt.query_row(params![docset_id], |row| row.get(0))?;
Ok(dict)
}
pub fn insert_entry(
&self,
docset_id: i64,
name: &str,
entry_type: &str,
path: &str,
) -> Result<i64, StorageError> {
self.conn.execute(
"INSERT INTO search_index (docset_id, name, type, path) \
VALUES (?1, ?2, ?3, ?4)",
params![docset_id, name, entry_type, path],
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn get_entry(&self, id: i64) -> Result<SearchEntry, StorageError> {
let mut stmt = self.conn.prepare(
"SELECT si.id, si.docset_id, si.name, si.type, si.path, si.usage_count, \
d.name, d.version \
FROM search_index si \
JOIN docsets d ON d.id = si.docset_id \
WHERE si.id = ?1",
)?;
Ok(stmt.query_row(params![id], row_to_entry)?)
}
pub fn list_entries_for_docset(
&self,
docset_id: i64,
) -> Result<Vec<SearchEntry>, StorageError> {
let mut stmt = self.conn.prepare(
"SELECT si.id, si.docset_id, si.name, si.type, si.path, si.usage_count, \
d.name, d.version \
FROM search_index si \
JOIN docsets d ON d.id = si.docset_id \
WHERE si.docset_id = ?1 ORDER BY si.name",
)?;
let rows = stmt.query_map(params![docset_id], row_to_entry)?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
pub fn list_all_entries(&self) -> Result<Vec<SearchEntry>, StorageError> {
let mut stmt = self.conn.prepare(
"SELECT si.id, si.docset_id, si.name, si.type, si.path, si.usage_count, \
d.name, d.version \
FROM search_index si \
JOIN docsets d ON d.id = si.docset_id \
ORDER BY si.name",
)?;
let rows = stmt.query_map([], row_to_entry)?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
pub fn find_docset_by_name(
&self,
name: &str,
version: Option<&str>,
) -> Result<Option<Docset>, StorageError> {
if let Some(v) = version {
let mut stmt = self.conn.prepare(
"SELECT id, name, version, path, is_favorite \
FROM docsets WHERE lower(name) = lower(?1) AND version = ?2 \
LIMIT 1",
)?;
let mut rows = stmt.query_map(params![name, v], row_to_docset)?;
Ok(rows.next().transpose()?)
} else {
let mut stmt = self.conn.prepare(
"SELECT id, name, version, path, is_favorite \
FROM docsets WHERE lower(name) = lower(?1) \
ORDER BY version DESC LIMIT 1",
)?;
let mut rows = stmt.query_map(params![name], row_to_docset)?;
Ok(rows.next().transpose()?)
}
}
pub fn find_entry_by_path(
&self,
docset_id: i64,
path: &str,
) -> Result<Option<SearchEntry>, StorageError> {
let base = path.split('#').next().unwrap_or(path);
let mut stmt = self.conn.prepare(
"SELECT si.id, si.docset_id, si.name, si.type, si.path, si.usage_count, \
d.name, d.version \
FROM search_index si \
JOIN docsets d ON d.id = si.docset_id \
WHERE si.docset_id = ?1 \
AND (si.path = ?2 OR si.path LIKE ?3 ESCAPE '\\') \
LIMIT 1",
)?;
let like_pattern = format!("{}%", escape_like(base));
let mut rows = stmt.query_map(params![docset_id, base, like_pattern], row_to_entry)?;
Ok(rows.next().transpose()?)
}
pub fn increment_usage(&self, index_id: i64) -> Result<(), StorageError> {
self.conn.execute(
"UPDATE search_index SET usage_count = usage_count + 1 WHERE id = ?1",
params![index_id],
)?;
Ok(())
}
pub fn insert_page(&self, index_id: i64, markdown: &str) -> Result<(), StorageError> {
let compressed =
encode_all(markdown.as_bytes(), self.compression_level).map_err(StorageError::Zstd)?;
self.conn.execute(
"INSERT OR REPLACE INTO pages (index_id, content) VALUES (?1, ?2)",
params![index_id, compressed],
)?;
Ok(())
}
pub fn get_page_content(&self, index_id: i64) -> Result<PageContent, StorageError> {
let mut stmt = self
.conn
.prepare("SELECT content FROM pages WHERE index_id = ?1")?;
let compressed: Vec<u8> = stmt.query_row(params![index_id], |row| row.get(0))?;
let decompressed = decode_all(Cursor::new(compressed)).map_err(StorageError::Zstd)?;
Ok(PageContent {
markdown: String::from_utf8(decompressed)?,
})
}
pub fn insert_page_dict(
&self,
index_id: i64,
markdown: &str,
dict: Option<&[u8]>,
) -> Result<(), StorageError> {
let compressed = compress_maybe_dict(markdown.as_bytes(), self.compression_level, dict)?;
self.conn.execute(
"INSERT OR REPLACE INTO pages (index_id, content) VALUES (?1, ?2)",
params![index_id, compressed],
)?;
Ok(())
}
pub fn get_page_content_dict(
&self,
index_id: i64,
dict: Option<&[u8]>,
) -> Result<PageContent, StorageError> {
let mut stmt = self
.conn
.prepare("SELECT content FROM pages WHERE index_id = ?1")?;
let compressed: Vec<u8> = stmt.query_row(params![index_id], |row| row.get(0))?;
let decompressed = decompress_maybe_dict(&compressed, dict)?;
Ok(PageContent {
markdown: String::from_utf8(decompressed)?,
})
}
pub fn insert_entry_favorite(
&self,
entry_id: i64,
entry_name: &str,
entry_type: &str,
entry_path: &str,
docset_id: i64,
docset_name: &str,
) -> Result<(), StorageError> {
self.conn.execute(
"INSERT OR REPLACE INTO entry_favorites \
(entry_id, entry_name, entry_type, entry_path, docset_id, docset_name) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
entry_id,
entry_name,
entry_type,
entry_path,
docset_id,
docset_name
],
)?;
Ok(())
}
pub fn delete_entry_favorite(&self, entry_id: i64) -> Result<(), StorageError> {
self.conn.execute(
"DELETE FROM entry_favorites WHERE entry_id = ?1",
params![entry_id],
)?;
Ok(())
}
pub fn list_entry_favorites(&self) -> Result<Vec<FavoriteEntry>, StorageError> {
let mut stmt = self.conn.prepare(
"SELECT entry_id, entry_name, entry_type, entry_path, docset_id, docset_name \
FROM entry_favorites \
ORDER BY docset_name, entry_name",
)?;
let rows = stmt.query_map([], |row| {
Ok(FavoriteEntry {
entry_id: row.get(0)?,
entry_name: row.get(1)?,
entry_type: row.get(2)?,
entry_path: row.get(3)?,
docset_id: row.get(4)?,
docset_name: row.get(5)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
}
fn escape_like(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if matches!(c, '\\' | '%' | '_') {
out.push('\\');
}
out.push(c);
}
out
}
fn row_to_docset(row: &rusqlite::Row<'_>) -> rusqlite::Result<Docset> {
Ok(Docset {
id: row.get(0)?,
name: row.get(1)?,
version: row.get(2)?,
path: row.get(3)?,
is_favorite: row.get::<_, i32>(4)? != 0,
})
}
fn row_to_entry(row: &rusqlite::Row<'_>) -> rusqlite::Result<SearchEntry> {
Ok(SearchEntry {
id: row.get(0)?,
docset_id: row.get(1)?,
name: row.get(2)?,
entry_type: row.get(3)?,
path: row.get(4)?,
usage_count: row.get(5)?,
docset_name: row.get(6)?,
docset_version: row.get(7)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn open_memory() -> Storage {
Storage::open(Path::new(":memory:"), 3).expect("in-memory storage must open")
}
#[test]
fn schema_initialises_without_error() {
open_memory();
}
#[test]
fn schema_is_idempotent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("docterm.sqlite");
Storage::open(&path, 3).unwrap();
Storage::open(&path, 3).unwrap();
}
#[test]
fn insert_docset_is_idempotent() {
let s = open_memory();
let id1 = s.insert_docset("Rust", Some("1.80.0"), None).unwrap();
let id2 = s.insert_docset("Rust", Some("1.80.0"), None).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn list_docsets_alphabetical_non_favorites() {
let s = open_memory();
s.insert_docset("Tokio", None, None).unwrap();
s.insert_docset("Rust", None, None).unwrap();
let list = s.list_docsets().unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list[0].name, "Rust");
assert_eq!(list[1].name, "Tokio");
}
#[test]
fn delete_docset_cascades_to_entries_and_pages() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "spawn", "Function", "tokio/fn.spawn.html")
.unwrap();
s.insert_page(eid, "# spawn").unwrap();
s.delete_docset(did).unwrap();
assert!(s.get_entry(eid).is_err());
assert!(s.get_page_content(eid).is_err());
}
#[test]
fn insert_and_get_entry() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
let e = s.get_entry(eid).unwrap();
assert_eq!(e.name, "Vec");
assert_eq!(e.entry_type, "Struct");
assert_eq!(e.docset_id, did);
assert_eq!(e.usage_count, 0);
}
#[test]
fn list_entries_for_docset_sorted() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
s.insert_entry(did, "Vec", "Struct", "vec.html").unwrap();
s.insert_entry(did, "Arc", "Struct", "arc.html").unwrap();
let entries = s.list_entries_for_docset(did).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "Arc");
assert_eq!(entries[1].name, "Vec");
}
#[test]
fn list_all_entries_across_docsets() {
let s = open_memory();
let d1 = s.insert_docset("Rust", None, None).unwrap();
let d2 = s.insert_docset("Tokio", None, None).unwrap();
s.insert_entry(d1, "Vec", "Struct", "vec.html").unwrap();
s.insert_entry(d2, "spawn", "Function", "spawn.html")
.unwrap();
let all = s.list_all_entries().unwrap();
assert_eq!(all.len(), 2);
let names: Vec<_> = all.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"Vec"));
assert!(names.contains(&"spawn"));
}
#[test]
fn find_docset_by_name_case_insensitive() {
let s = open_memory();
s.insert_docset("Rust", Some("1.80.0"), None).unwrap();
let found = s.find_docset_by_name("rust", None).unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().name, "Rust");
}
#[test]
fn find_docset_by_name_with_version() {
let s = open_memory();
s.insert_docset("Rust", Some("1.80.0"), None).unwrap();
s.insert_docset("Rust", Some("1.94.0"), None).unwrap();
let found = s.find_docset_by_name("Rust", Some("1.80.0")).unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().version.as_deref(), Some("1.80.0"));
}
#[test]
fn find_docset_by_name_returns_none_when_missing() {
let s = open_memory();
let found = s.find_docset_by_name("Python", None).unwrap();
assert!(found.is_none());
}
#[test]
fn increment_usage_accumulates() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
s.increment_usage(eid).unwrap();
s.increment_usage(eid).unwrap();
s.increment_usage(eid).unwrap();
assert_eq!(s.get_entry(eid).unwrap().usage_count, 3);
}
#[test]
fn page_round_trip() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
let original = "# Vec\n\nA contiguous growable array type.\n\n## Examples\n\n```rust\nlet v = vec![1, 2, 3];\n```";
s.insert_page(eid, original).unwrap();
assert_eq!(s.get_page_content(eid).unwrap().markdown, original);
}
#[test]
fn insert_page_upserts() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
s.insert_page(eid, "# Vec v1").unwrap();
s.insert_page(eid, "# Vec v2").unwrap();
assert_eq!(s.get_page_content(eid).unwrap().markdown, "# Vec v2");
}
fn make_dict() -> Vec<u8> {
let samples: Vec<&str> = (0..50)
.map(|i| {
Box::leak(
format!(
"# Item {i}\n\nDescription of item {i}.\n\n```rust\nlet x = {i};\n```\n"
)
.into_boxed_str(),
) as &str
})
.collect();
crate::postprocess::train_dictionary(&samples, 16 * 1024).unwrap()
}
#[test]
fn set_and_get_docset_dictionary() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
assert!(s.get_docset_dictionary(did).unwrap().is_none());
let dict = make_dict();
s.set_docset_dictionary(did, &dict).unwrap();
let retrieved = s.get_docset_dictionary(did).unwrap().unwrap();
assert_eq!(retrieved, dict);
}
#[test]
fn page_round_trip_with_dictionary() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
let dict = make_dict();
let original = "# Vec\n\nA growable array.\n\n```rust\nlet v = vec![1, 2, 3];\n```";
s.insert_page_dict(eid, original, Some(&dict)).unwrap();
let page = s.get_page_content_dict(eid, Some(&dict)).unwrap();
assert_eq!(page.markdown, original);
}
#[test]
fn insert_page_dict_none_falls_back_to_plain_zstd() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Arc", "Struct", "std/sync/struct.Arc.html")
.unwrap();
let original = "# Arc\n\nAtomically reference-counted pointer.";
s.insert_page_dict(eid, original, None).unwrap();
let page = s.get_page_content_dict(eid, None).unwrap();
assert_eq!(page.markdown, original);
}
fn insert_test_entry(s: &Storage, docset_id: i64, name: &str) -> i64 {
s.insert_entry(docset_id, name, "Function", &format!("{name}.html"))
.unwrap()
}
#[test]
fn insert_and_list_favorites() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = insert_test_entry(&s, did, "spawn");
s.insert_entry_favorite(eid, "spawn", "Function", "spawn.html", did, "Rust")
.unwrap();
let favs = s.list_entry_favorites().unwrap();
assert_eq!(favs.len(), 1);
assert_eq!(favs[0].entry_id, eid);
assert_eq!(favs[0].entry_name, "spawn");
assert_eq!(favs[0].docset_name, "Rust");
}
#[test]
fn delete_entry_favorite() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = insert_test_entry(&s, did, "Arc");
s.insert_entry_favorite(eid, "Arc", "Struct", "arc.html", did, "Rust")
.unwrap();
s.delete_entry_favorite(eid).unwrap();
assert!(s.list_entry_favorites().unwrap().is_empty());
}
#[test]
fn insert_favorite_is_upsert() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = insert_test_entry(&s, did, "Vec");
s.insert_entry_favorite(eid, "Vec", "Struct", "vec.html", did, "Rust")
.unwrap();
s.insert_entry_favorite(eid, "Vec", "Struct", "vec.html", did, "Rust")
.unwrap();
assert_eq!(s.list_entry_favorites().unwrap().len(), 1);
}
#[test]
fn favorite_cascades_on_entry_delete() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = insert_test_entry(&s, did, "Vec");
s.insert_entry_favorite(eid, "Vec", "Struct", "vec.html", did, "Rust")
.unwrap();
s.delete_docset(did).unwrap();
assert!(s.list_entry_favorites().unwrap().is_empty());
}
#[test]
fn favorites_ordered_by_docset_then_name() {
let s = open_memory();
let d1 = s.insert_docset("Tokio", None, None).unwrap();
let d2 = s.insert_docset("Rust", None, None).unwrap();
let e1 = insert_test_entry(&s, d1, "spawn");
let e2 = insert_test_entry(&s, d2, "Vec");
let e3 = insert_test_entry(&s, d2, "Arc");
s.insert_entry_favorite(e1, "spawn", "Function", "spawn.html", d1, "Tokio")
.unwrap();
s.insert_entry_favorite(e2, "Vec", "Struct", "vec.html", d2, "Rust")
.unwrap();
s.insert_entry_favorite(e3, "Arc", "Struct", "arc.html", d2, "Rust")
.unwrap();
let favs = s.list_entry_favorites().unwrap();
assert_eq!(favs[0].entry_name, "Arc"); assert_eq!(favs[1].entry_name, "Vec"); assert_eq!(favs[2].entry_name, "spawn"); }
#[test]
fn find_entry_by_path_exact_match() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
let found = s
.find_entry_by_path(did, "std/vec/struct.Vec.html")
.unwrap();
assert_eq!(found.unwrap().id, eid);
}
#[test]
fn find_entry_by_path_ignores_anchor_in_query() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(did, "Vec", "Struct", "std/vec/struct.Vec.html")
.unwrap();
let found = s
.find_entry_by_path(did, "std/vec/struct.Vec.html#method.new")
.unwrap();
assert_eq!(found.unwrap().id, eid);
}
#[test]
fn find_entry_by_path_matches_stored_anchor_via_prefix() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
let eid = s
.insert_entry(
did,
"Vec::new",
"Method",
"std/vec/struct.Vec.html#method.new",
)
.unwrap();
let found = s
.find_entry_by_path(did, "std/vec/struct.Vec.html")
.unwrap();
assert_eq!(found.unwrap().id, eid);
}
#[test]
fn find_entry_by_path_underscore_is_literal_not_wildcard() {
let s = open_memory();
let did = s.insert_docset("Python", None, None).unwrap();
let target = s
.insert_entry(did, "hash_map", "Function", "lib/hash_map.html")
.unwrap();
let decoy = s
.insert_entry(did, "hashXmap", "Function", "lib/hashXmap.html")
.unwrap();
assert_ne!(target, decoy);
let found = s.find_entry_by_path(did, "lib/hashXmap.html").unwrap();
assert_eq!(found.unwrap().id, decoy);
let found = s.find_entry_by_path(did, "lib/hash_map.html").unwrap();
assert_eq!(found.unwrap().id, target);
}
#[test]
fn find_entry_by_path_percent_is_literal_not_wildcard() {
let s = open_memory();
let did = s.insert_docset("Weird", None, None).unwrap();
let literal = s.insert_entry(did, "pct", "Function", "a%b.html").unwrap();
let decoy = s
.insert_entry(did, "long", "Function", "aXXXb.html")
.unwrap();
let found = s.find_entry_by_path(did, "a%b.html").unwrap();
assert_eq!(found.unwrap().id, literal);
let found = s.find_entry_by_path(did, "aXXXb.html").unwrap();
assert_eq!(found.unwrap().id, decoy);
}
#[test]
fn find_entry_by_path_backslash_is_literal() {
let s = open_memory();
let did = s.insert_docset("Weird", None, None).unwrap();
let eid = s.insert_entry(did, "bs", "Function", "a\\b.html").unwrap();
let found = s.find_entry_by_path(did, "a\\b.html").unwrap();
assert_eq!(found.unwrap().id, eid);
}
#[test]
fn find_entry_by_path_returns_none_when_missing() {
let s = open_memory();
let did = s.insert_docset("Rust", None, None).unwrap();
assert!(s.find_entry_by_path(did, "nope.html").unwrap().is_none());
}
#[test]
fn escape_like_passes_plain_text_unchanged() {
assert_eq!(escape_like("foo/bar.html"), "foo/bar.html");
}
#[test]
fn escape_like_escapes_wildcards_and_backslash() {
assert_eq!(escape_like("a%b_c\\d"), r"a\%b\_c\\d");
}
#[test]
fn storage_error_display_utf8() {
let e = StorageError::Utf8(String::from_utf8(vec![0xFF]).unwrap_err());
assert!(!e.to_string().is_empty());
}
#[test]
fn storage_error_display_zstd() {
let io_err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated");
let e = StorageError::Zstd(io_err);
assert!(e.to_string().contains("compression"));
}
}