add delete dict operations

This commit is contained in:
polv committed 2026-02-23 17:27:41 +07:00
1 parent fa5bfb56b0
commit 1e91f21ca4
8 files changed
+129 -44

No files matched your search

@@ -31,7 +31,7 @@ fn main() -> Result<(), anyhow::Error> {
"{:?}", "{:?}",
AppService::import_yomitan_zip_file(&mut writer, p, "ja")?, AppService::import_yomitan_zip_file(&mut writer, p, "ja")?,
); );
println!("[{:.2?}]", start.elapsed(),); println!("[{:.2?}]", start.elapsed());
} else { } else {
println!("not zip_file: {:?}", p); println!("not zip_file: {:?}", p);
} }
@@ -10,7 +10,8 @@ fn main() -> Result<(), anyhow::Error> {
let service = AppService::new(get_db_dir())?; let service = AppService::new(get_db_dir())?;
let start = Instant::now(); let start = Instant::now();
service.get_yomitan_writer()?; let mut writer = service.get_yomitan_writer()?;
AppService::remove_yomitan_dictionary(&mut writer, "JMnedict")?;
println!("[{:.2?}]", start.elapsed()); println!("[{:.2?}]", start.elapsed());
Ok(()) Ok(())
File renamed without changes.
+88 -34
View File
@@ -1,10 +1,9 @@
use std::{collections::HashMap, fs::File, io::Read, path::PathBuf};
use anyhow::Context; use anyhow::Context;
use blake3::hash; use blake3::hash;
use rusqlite::{Connection, Result, Transaction, params}; use rusqlite::{Connection, Result, Transaction, params};
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use std::{collections::HashMap, fs::File, io::Read, path::PathBuf, time::Instant};
use zip::ZipArchive; use zip::ZipArchive;
use crate::CJDicError; use crate::CJDicError;
@@ -25,19 +24,23 @@ pub struct YomitanWriter {
} }
impl YomitanWriter { impl YomitanWriter {
pub fn new(conn: Connection) -> Self { pub fn new(conn: Connection) -> Result<Self> {
Self { conn } conn.execute_batch(
} "
pub fn create_schema(&self) -> Result<()> {
self.conn.execute_batch(
"
PRAGMA journal_mode = WAL; PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -65536; PRAGMA cache_size = -65536;
PRAGMA temp_store = MEMORY; PRAGMA temp_store = MEMORY;
",
)?;
Ok(Self { conn })
}
pub fn create_schema(&self) -> Result<()> {
self.conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS schema_meta ( CREATE TABLE IF NOT EXISTS schema_meta (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL value TEXT NOT NULL
@@ -107,11 +110,18 @@ impl YomitanWriter {
self.conn.execute_batch( self.conn.execute_batch(
" "
CREATE INDEX IF NOT EXISTS dictionaries_lang ON dictionaries (lang); CREATE INDEX IF NOT EXISTS dictionaries_lang ON dictionaries (lang);
CREATE INDEX IF NOT EXISTS terms_term ON terms (term); CREATE INDEX IF NOT EXISTS terms_dict_id ON terms (dict_id);
CREATE INDEX IF NOT EXISTS terms_reading ON terms (reading); CREATE INDEX IF NOT EXISTS terms_term ON terms (term);
", CREATE INDEX IF NOT EXISTS terms_reading ON terms (reading);
CREATE INDEX IF NOT EXISTS term_meta_dict_id ON term_meta (dict_id);
CREATE INDEX IF NOT EXISTS tags_dict_id ON tags (dict_id);
CREATE INDEX IF NOT EXISTS kanji_dict_id ON kanji (dict_id);
",
)?; )?;
let mut stmt = self let mut stmt = self
@@ -131,12 +141,15 @@ impl YomitanWriter {
Ok(()) Ok(())
} }
pub fn import_bundled_zip_file( pub fn import_dictionary_zip_file(
&mut self, &mut self,
zip_file: PathBuf, zip_file: PathBuf,
lang: &str, lang: &str,
) -> anyhow::Result<YomitanZipImportResult, CJDicError> { ) -> anyhow::Result<YomitanZipImportResult, CJDicError> {
self.create_schema()?; self.create_schema()?; // < 1 ms
let start_time = Instant::now();
self.conn.execute_batch("PRAGMA foreign_keys = off")?;
let f = let f =
File::open(&zip_file).with_context(|| format!("opening zip {}", zip_file.display()))?; File::open(&zip_file).with_context(|| format!("opening zip {}", zip_file.display()))?;
@@ -159,36 +172,57 @@ impl YomitanWriter {
} }
}; };
let title = index_file fn remove_timestamp(s: &str) -> String {
.get("title") if let Some(pos) = s.rfind('[') {
.and_then(Value::as_str) // Trim whitespace only before the '[' and return the substring
.unwrap_or("") let trimmed_before_bracket = s[..pos].trim_end();
.to_string(); trimmed_before_bracket.to_string()
} else {
s.to_string() // Return the original string if no '[' is found
}
}
let title = remove_timestamp(
&index_file
.get("title")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
);
let revision = index_file let revision = index_file
.get("revision") .get("revision")
.and_then(Value::as_str) .and_then(Value::as_str)
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
// Skip if already installed {
let exists: bool = self let title: &str = &title;
.conn // Skip if already installed
.prepare("SELECT 1 FROM dictionaries WHERE title = ?1 AND revision = ?2")? let mut stmt = self
.exists([title, revision])?; .conn
if exists { .prepare("SELECT (revision < ?2) FROM dictionaries WHERE title = ?1")?;
return Ok(YomitanZipImportResult { let mut rows = stmt.query(params![title, revision])?;
exists: true, if let Some(row) = rows.next()? {
load: false, let is_outdated: bool = row.get(0)?;
error: None, if is_outdated {
}); self.drop_dictionary(title)?;
} else {
return Ok(YomitanZipImportResult {
exists: true,
load: false,
error: None,
});
}
}
} }
let tx = self.conn.transaction()?; let tx = self.conn.transaction()?;
tx.execute( tx.execute(
"INSERT INTO dictionaries (title, revision, author, url, description, lang) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", "INSERT INTO dictionaries (title, revision, author, url, description, lang) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![ params![
index_file.get("title").and_then(Value::as_str), title,
index_file.get("revision").and_then(Value::as_str), revision,
index_file.get("author").and_then(Value::as_str), index_file.get("author").and_then(Value::as_str),
index_file.get("url").and_then(Value::as_str), index_file.get("url").and_then(Value::as_str),
index_file.get("description").and_then(Value::as_str), index_file.get("description").and_then(Value::as_str),
@@ -377,7 +411,20 @@ impl YomitanWriter {
tx.commit()?; tx.commit()?;
self.conn.execute_batch("VACUUM;")?; println!(
"Importing {:?} takes {:.2?}",
zip_file,
start_time.elapsed()
);
self.conn.execute_batch(
"
PRAGMA foreign_keys = on;
PRAGMA check_foreign_keys;
",
)?; // < 1 ms
// self.conn.execute_batch("VACUUM;")?; // Not needed, twice time taken than import
Ok(YomitanZipImportResult { Ok(YomitanZipImportResult {
exists: true, exists: true,
@@ -385,6 +432,13 @@ impl YomitanWriter {
error: None, error: None,
}) })
} }
pub fn drop_dictionary(&self, title: &str) -> Result<()> {
self.conn
.execute("DELETE FROM dictionaries WHERE title = ?1", [title])?;
Ok(())
}
} }
fn intern( fn intern(
+9 -2
View File
@@ -43,7 +43,7 @@ impl AppService {
pub fn get_yomitan_writer(&self) -> Result<YomitanWriter, CJDicError> { pub fn get_yomitan_writer(&self) -> Result<YomitanWriter, CJDicError> {
let conn = Connection::open(self.db.dir.join("yomitan.db"))?; let conn = Connection::open(self.db.dir.join("yomitan.db"))?;
let writer = YomitanWriter::new(conn); let writer = YomitanWriter::new(conn)?;
writer.create_schema()?; writer.create_schema()?;
Ok(writer) Ok(writer)
} }
@@ -53,6 +53,13 @@ impl AppService {
zip_file: PathBuf, zip_file: PathBuf,
lang: &str, lang: &str,
) -> Result<YomitanZipImportResult, CJDicError> { ) -> Result<YomitanZipImportResult, CJDicError> {
Ok(writer.import_bundled_zip_file(zip_file, lang)?) Ok(writer.import_dictionary_zip_file(zip_file, lang)?)
}
pub fn remove_yomitan_dictionary(
writer: &mut YomitanWriter,
title: &str,
) -> Result<(), CJDicError> {
Ok(writer.drop_dictionary(title)?)
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

+15
View File
@@ -1,9 +1,24 @@
a {
text-decoration: underline;
color: teal;
}
span {
display: inline-flex;
gap: 0.5em;
align-items: center;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
color: #f6f6f6; color: #f6f6f6;
background-color: #2f2f2f; background-color: #2f2f2f;
} }
a {
color: lightgray;
}
a:hover { a:hover {
color: #24c8db; color: #24c8db;
} }
+14 -6
View File
@@ -68,12 +68,20 @@ function App() {
<Card.Root> <Card.Root>
<Card.Header>{JSON.stringify(it)}</Card.Header> <Card.Header>{JSON.stringify(it)}</Card.Header>
<Card.Body> <Card.Body>
{JSON.parse(glossary_json).map((g: any) => ( <div
<Glossary style={{
glossary={g} gap: "1em",
onTermClicked={(t) => setQ(t + " ")} display: "flex",
/> flexDirection: "column",
))} }}
>
{JSON.parse(glossary_json).map((g: any) => (
<Glossary
glossary={g}
onTermClicked={(t) => setQ(t + " ")}
/>
))}
</div>
</Card.Body> </Card.Body>
</Card.Root> </Card.Root>
</details> </details>