use std::collections::BTreeMap;
use std::path::Path;
use rusqlite::{Connection, OpenFlags, OptionalExtension};
use serde_json::{json, Value};
use super::request::{member, Request};
use super::{BackendError, Outcome};
use crate::format;
use crate::pinyin::segment::split_segments;
pub(super) fn open_read_only(path: &Path) -> Result<Connection, BackendError> {
let path = crate::paths::sqlite_read_only_path_no_follow(path)
.map_err(|_| BackendError::ResourcesUnavailable)?;
Connection::open_with_flags(
&path,
OpenFlags::SQLITE_OPEN_READ_ONLY
| OpenFlags::SQLITE_OPEN_NOFOLLOW
| OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|_| BackendError::ResourcesUnavailable)
}
pub(super) fn table_exists(connection: &Connection, table: &str) -> Result<bool, BackendError> {
Ok(connection
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1",
[table],
|_| Ok(()),
)
.optional()?
.is_some())
}
pub(super) fn listed_pinyin_batch(request: &Request, dictionary: &Path) -> Outcome {
let entries = request.batch("entries")?;
let connection = open_read_only(dictionary)?;
let mut listed = Vec::with_capacity(entries.len());
for entry in entries {
let code = member(entry, "code")?;
let word = member(entry, "word")?;
let table =
format::build_table_name(&split_segments(code)).ok_or(BackendError::InvalidRequest)?;
if word.is_empty() {
return Err(BackendError::InvalidRequest);
}
if !table_exists(&connection, &table)? {
listed.push(false);
continue;
}
let found = connection
.query_row(
&format!("SELECT 1 FROM \"{table}\" WHERE key=?1 AND value=?2 LIMIT 1"),
[code, word],
|_| Ok(()),
)
.optional()?
.is_some();
listed.push(found);
}
Ok(json!({ "listed": listed }))
}
pub(super) fn listed_english_batch(request: &Request, dictionary: &Path) -> Outcome {
let entries = request.batch("entries")?;
let connection = open_read_only(dictionary)?;
let mut statement = connection
.prepare("SELECT 1 FROM english_words WHERE word=?1 AND display=?2 LIMIT 1")
.map_err(|_| BackendError::ResourcesUnavailable)?;
let mut listed = Vec::with_capacity(entries.len());
for entry in entries {
let word = member(entry, "word")?;
let display = member(entry, "display")?;
if word.is_empty() || display.is_empty() {
return Err(BackendError::InvalidRequest);
}
listed.push(
statement
.query_row([word, display], |_| Ok(()))
.optional()?
.is_some(),
);
}
Ok(json!({ "listed": listed }))
}
pub(super) fn pinyin_weight_medians(dictionary: &Path) -> Outcome {
let connection = open_read_only(dictionary)?;
let mut medians = BTreeMap::new();
let mut weights: Vec<i64> = Vec::new();
for syllables in 1..=format::MAXIMUM_NUMBERED_SYLLABLES + 1 {
weights.clear();
for initial in format::SHIPPED_INITIALS.bytes() {
let Some(table) = format::quanpin_table(syllables, initial) else {
continue;
};
if !table_exists(&connection, &table)? {
continue;
}
let mut statement = connection.prepare(&format!(
"SELECT weight FROM \"{table}\" WHERE weight IS NOT NULL"
))?;
let rows = statement.query_map([], |row| row.get::<_, i64>(0))?;
for weight in rows {
weights.push(weight?);
}
}
if weights.is_empty() {
continue;
}
let middle = (weights.len() - 1) / 2;
let (_, median, _) = weights.select_nth_unstable(middle);
medians.insert(syllables.to_string(), Value::from(*median));
}
if medians.is_empty() {
return Err(BackendError::ResourcesUnavailable);
}
Ok(json!({ "medians": medians }))
}