1use std::path::Path;
6
7use rusqlite::{types::Type, Connection, OpenFlags, OptionalExtension};
8
9use crate::diagnostics;
10use crate::error::{EngineError, Result};
11
12pub const FORMAT_VERSION: u32 = 1;
14
15pub const SCHEMA: &str = "\
17CREATE TABLE metadata(name TEXT PRIMARY KEY, value TEXT NOT NULL) WITHOUT ROWID;
18CREATE TABLE syllables(syllable TEXT PRIMARY KEY) WITHOUT ROWID;
19CREATE TABLE entries(key TEXT NOT NULL, text TEXT NOT NULL, weight INTEGER NOT NULL, PRIMARY KEY(key, text)) WITHOUT ROWID;
20CREATE INDEX entries_by_key_weight ON entries(key, weight DESC);
21";
22
23pub const METADATA_FORMAT_VERSION: &str = "format_version";
25pub const METADATA_SOURCE_COMMIT: &str = "source_commit";
27pub const METADATA_LICENSE: &str = "license";
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct LanguageEntry {
33 pub text: String,
34 pub weight: i64,
35}
36
37pub struct LanguageDictionary {
39 connection: Connection,
40}
41
42pub fn open_read_only(path: &Path) -> Result<LanguageDictionary> {
44 if path.as_os_str().is_empty() {
46 return Err(EngineError::failed(
47 diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE,
48 ));
49 }
50 let unavailable =
51 |_: rusqlite::Error| EngineError::failed(diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE);
52 let path = crate::paths::sqlite_path_no_follow_allow_parent_symlinks(path)
54 .map_err(|_| EngineError::failed(diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE))?;
55 let connection = Connection::open_with_flags(
56 &path,
57 OpenFlags::SQLITE_OPEN_READ_ONLY
58 | OpenFlags::SQLITE_OPEN_NOFOLLOW
59 | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
60 )
61 .map_err(unavailable)?;
62 connection
63 .busy_timeout(std::time::Duration::ZERO)
64 .map_err(unavailable)?;
65 let dictionary = LanguageDictionary { connection };
66 let version = dictionary
68 .metadata(METADATA_FORMAT_VERSION)
69 .map_err(|_| EngineError::failed(diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE))?;
70 if version.as_deref() != Some(FORMAT_VERSION.to_string().as_str()) {
71 return Err(EngineError::failed(
72 diagnostics::LANGUAGE_DICTIONARY_VERSION_UNSUPPORTED,
73 ));
74 }
75 Ok(dictionary)
76}
77
78impl LanguageDictionary {
79 pub(crate) fn browse(
81 &self,
82 query: &str,
83 offset: usize,
84 limit: usize,
85 ) -> Result<Vec<crate::dictionary::DictRow>> {
86 let query = query.to_ascii_lowercase();
87 let mut statement = self.connection.prepare_cached(
88 "SELECT key,text,weight FROM entries WHERE ?1='' OR instr(key,?1)=1 OR instr(text,?1)>0 ORDER BY key,text LIMIT ?2 OFFSET ?3",
89 )?;
90 let rows = statement.query_map((query, limit as i64, offset as i64), |row| {
91 Ok(crate::dictionary::DictRow {
92 key: row.get(0)?,
93 value: row.get(1)?,
94 weight: row.get(2)?,
95 })
96 })?;
97 Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
98 }
99
100 pub fn metadata(&self, name: &str) -> Result<Option<String>> {
102 Ok(self
103 .connection
104 .prepare_cached("SELECT value FROM metadata WHERE name = ?1")?
105 .query_row((name,), |row| row.get(0))
106 .optional()?)
107 }
108
109 pub fn lookup(&self, key: &str, limit: usize) -> Result<Vec<LanguageEntry>> {
111 let mut result = Vec::new();
112 self.lookup_into(key, limit, &mut result)?;
113 Ok(result)
114 }
115
116 pub fn lookup_into(
118 &self,
119 key: &str,
120 limit: usize,
121 result: &mut Vec<LanguageEntry>,
122 ) -> Result<()> {
123 let capacity = query_capacity(limit);
124 let limit = i64::try_from(limit).unwrap_or(i64::MAX);
125 let mut statement = self.connection.prepare_cached(
126 "SELECT text, weight FROM entries WHERE key = ?1 ORDER BY weight DESC, text ASC LIMIT ?2",
127 )?;
128 let mut length = 0;
129 let mut rows = statement.query((key, limit))?;
130 while let Some(row) = rows.next()? {
131 let text = row.get_ref(0)?.as_str().map_err(|error| {
132 rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
133 })?;
134 let weight = row.get(1)?;
135 reserve_query_capacity(result, length, capacity);
136 if let Some(entry) = result.get_mut(length) {
137 entry.text.clear();
138 entry.text.push_str(text);
139 entry.weight = weight;
140 } else {
141 result.push(LanguageEntry {
142 text: text.to_owned(),
143 weight,
144 });
145 }
146 length += 1;
147 }
148 result.truncate(length);
149 Ok(())
150 }
151
152 pub fn has_syllable(&self, syllable: &str) -> Result<bool> {
154 Ok(self
155 .connection
156 .prepare_cached("SELECT 1 FROM syllables WHERE syllable = ?1")?
157 .exists((syllable,))?)
158 }
159
160 pub fn syllables(&self) -> Result<Vec<String>> {
162 let mut statement = self
163 .connection
164 .prepare_cached("SELECT syllable FROM syllables")?;
165 let rows = statement.query_map((), |row| row.get(0))?;
166 Ok(rows.collect::<rusqlite::Result<_>>()?)
167 }
168
169 pub fn lookup_completions(
171 &self,
172 prefix: &str,
173 limit: usize,
174 ) -> Result<Vec<(String, LanguageEntry)>> {
175 let mut result = Vec::new();
176 self.lookup_completions_into(prefix, limit, &mut result)?;
177 Ok(result)
178 }
179
180 pub fn lookup_completions_into(
182 &self,
183 prefix: &str,
184 limit: usize,
185 result: &mut Vec<(String, LanguageEntry)>,
186 ) -> Result<()> {
187 let capacity = query_capacity(limit);
188 let Some(upper) = completion_upper_bound(prefix) else {
190 result.clear();
191 return Ok(());
192 };
193 let limit = i64::try_from(limit).unwrap_or(i64::MAX);
194 let mut statement = self.connection.prepare_cached(
195 "SELECT key, text, weight FROM entries WHERE key >= ?1 AND key < ?2 AND instr(substr(key, length(?1) + 1), ' ') = 0 ORDER BY weight DESC, text ASC LIMIT ?3",
196 )?;
197 let mut length = 0;
198 let mut rows = statement.query((prefix, upper, limit))?;
199 while let Some(row) = rows.next()? {
200 let key = row.get_ref(0)?.as_str().map_err(|error| {
201 rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
202 })?;
203 let text = row.get_ref(1)?.as_str().map_err(|error| {
204 rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error))
205 })?;
206 let weight = row.get(2)?;
207 reserve_query_capacity(result, length, capacity);
208 if let Some((existing_key, entry)) = result.get_mut(length) {
209 existing_key.clear();
210 existing_key.push_str(key);
211 entry.text.clear();
212 entry.text.push_str(text);
213 entry.weight = weight;
214 } else {
215 result.push((
216 key.to_owned(),
217 LanguageEntry {
218 text: text.to_owned(),
219 weight,
220 },
221 ));
222 }
223 length += 1;
224 }
225 result.truncate(length);
226 Ok(())
227 }
228
229 pub fn texts_with_key_prefix(&self, prefix: &str) -> Result<Vec<String>> {
231 let Some(upper) = completion_upper_bound(prefix) else {
232 return Ok(Vec::new());
233 };
234 let mut statement = self
235 .connection
236 .prepare_cached("SELECT text FROM entries WHERE key >= ?1 AND key < ?2")?;
237 let rows = statement.query_map((prefix, upper), |row| row.get(0))?;
238 Ok(rows.collect::<rusqlite::Result<_>>()?)
239 }
240
241 pub fn lookup_pattern(
243 &self,
244 pattern: &str,
245 wildcard: char,
246 completions: bool,
247 limit: usize,
248 ) -> Result<Vec<(String, LanguageEntry)>> {
249 if pattern.is_empty() {
250 return Ok(Vec::new());
251 }
252 let mut result = Vec::new();
253 self.lookup_pattern_into(pattern, wildcard, completions, limit, &mut result)?;
254 Ok(result)
255 }
256
257 pub fn lookup_pattern_into(
259 &self,
260 pattern: &str,
261 wildcard: char,
262 completions: bool,
263 limit: usize,
264 result: &mut Vec<(String, LanguageEntry)>,
265 ) -> Result<()> {
266 if pattern.is_empty() {
267 result.clear();
268 return Ok(());
269 }
270 let glob = glob_pattern(pattern, wildcard, completions);
271 let length = i64::try_from(pattern.chars().count()).unwrap_or(i64::MAX);
272 let literal = pattern.split(wildcard).next().unwrap_or_default();
273 let capacity = query_capacity(limit);
274 let limit = i64::try_from(limit).unwrap_or(i64::MAX);
275 match completion_upper_bound(literal) {
277 Some(upper) => {
278 let mut statement = self.connection.prepare_cached(
279 "SELECT key, text, weight FROM entries WHERE key >= ?1 AND key < ?2 AND key GLOB ?3 AND instr(substr(key, ?4 + 1), ' ') = 0 ORDER BY weight DESC, text ASC LIMIT ?5",
280 )?;
281 read_keyed_rows_into(
282 &mut statement.query((literal, upper, glob, length, limit))?,
283 result,
284 capacity,
285 )?;
286 }
287 None => {
288 let mut statement = self.connection.prepare_cached(
289 "SELECT key, text, weight FROM entries WHERE key GLOB ?1 AND instr(substr(key, ?2 + 1), ' ') = 0 ORDER BY weight DESC, text ASC LIMIT ?3",
290 )?;
291 read_keyed_rows_into(
292 &mut statement.query((glob, length, limit))?,
293 result,
294 capacity,
295 )?;
296 }
297 }
298 Ok(())
299 }
300
301 pub fn lookup_readings(
303 &self,
304 positions: &[&[String]],
305 limit: usize,
306 ) -> Result<Vec<(String, LanguageEntry)>> {
307 let Some((first, rest)) = positions.split_first() else {
308 return Ok(Vec::new());
309 };
310 if positions.iter().all(|readings| readings.len() == 1) {
311 let key = join_single_readings(positions);
312 let mut entries = self.lookup(&key, limit)?;
313 if entries.len() <= 1 {
314 return Ok(entries
315 .pop()
316 .map(|entry| vec![(key, entry)])
317 .unwrap_or_default());
318 }
319 let mut entries = entries.into_iter();
320 let first = entries.next().expect("多结果查询至少有一条结果");
321 let mut result = Vec::with_capacity(entries.len() + 1);
322 result.push((key, first));
323 for entry in entries {
324 result.push((result[0].0.clone(), entry));
325 }
326 return Ok(result);
327 }
328 let mut result = Vec::new();
329 if rest.is_empty() {
330 let limit = i64::try_from(limit).unwrap_or(i64::MAX);
331 let mut statement = self.connection.prepare_cached(
332 "SELECT text, weight FROM entries WHERE key = ?1 ORDER BY weight DESC, text ASC LIMIT ?2",
333 )?;
334 for reading in first.iter() {
335 let mut rows = statement.query((reading, limit))?;
336 while let Some(row) = rows.next()? {
337 result.push((
338 reading.clone(),
339 LanguageEntry {
340 text: row.get(0)?,
341 weight: row.get(1)?,
342 },
343 ));
344 }
345 }
346 } else {
347 let tail = readings_glob(rest);
349 let mut statement = self.connection.prepare_cached(
350 "SELECT key, text, weight FROM entries WHERE key >= ?1 AND key < ?2 AND key GLOB ?3",
351 )?;
352 let mut lower = String::new();
353 let mut upper = String::new();
354 let mut glob = String::new();
355 for reading in first.iter() {
356 lower.clear();
358 lower.reserve_exact(reading.len() + 1);
359 lower.push_str(reading);
360 lower.push(' ');
361 upper.clear();
362 upper.reserve_exact(reading.len() + 1);
363 upper.push_str(reading);
364 upper.push('!');
365 glob.clear();
366 glob.reserve_exact(reading.len() * 2 + 1 + tail.len());
367 push_glob_literal(&mut glob, reading);
368 glob.push(' ');
369 glob.push_str(&tail);
370 let mut rows = statement.query((lower.as_str(), upper.as_str(), glob.as_str()))?;
371 while let Some(row) = rows.next()? {
372 let key = row.get_ref(0)?.as_str().map_err(|error| {
373 rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
374 })?;
375 let text = row.get_ref(1)?.as_str().map_err(|error| {
376 rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error))
377 })?;
378 let weight = row.get(2)?;
379 if key_matches(key, positions) {
380 result.push((
381 key.to_owned(),
382 LanguageEntry {
383 text: text.to_owned(),
384 weight,
385 },
386 ));
387 }
388 }
389 }
390 }
391 result.sort_by(|(left_key, left), (right_key, right)| {
392 right
393 .weight
394 .cmp(&left.weight)
395 .then_with(|| left.text.cmp(&right.text))
396 .then_with(|| left_key.cmp(right_key))
397 });
398 result.truncate(limit);
399 Ok(result)
400 }
401}
402
403fn join_single_readings(positions: &[&[String]]) -> String {
404 let capacity = positions
405 .iter()
406 .map(|readings| readings[0].len())
407 .sum::<usize>()
408 .saturating_add(positions.len().saturating_sub(1));
409 let mut key = String::with_capacity(capacity);
410 for (index, readings) in positions.iter().enumerate() {
411 if index != 0 {
412 key.push(' ');
413 }
414 key.push_str(&readings[0]);
415 }
416 key
417}
418
419fn readings_glob(positions: &[&[String]]) -> String {
421 let mut glob = String::new();
422 for (index, readings) in positions.iter().enumerate() {
423 if index > 0 {
424 glob.push(' ');
425 }
426 if let [reading] = readings {
427 push_glob_literal(&mut glob, reading);
428 continue;
429 }
430 let lengths: Vec<usize> = readings
431 .iter()
432 .map(|reading| reading.chars().count())
433 .collect();
434 if lengths.windows(2).any(|pair| pair[0] != pair[1]) {
435 glob.push('*');
436 continue;
437 }
438 for position in 0..lengths.first().copied().unwrap_or_default() {
439 let mut class: Vec<char> = readings
440 .iter()
441 .filter_map(|reading| reading.chars().nth(position))
442 .collect();
443 class.sort_unstable();
444 class.dedup();
445 match class.as_slice() {
446 [only] => push_glob_literal(&mut glob, &only.to_string()),
447 _ if class
448 .iter()
449 .any(|character| matches!(character, ']' | '^' | '-')) =>
450 {
451 glob.push('?');
452 }
453 _ => {
454 glob.push('[');
455 glob.extend(class);
456 glob.push(']');
457 }
458 }
459 }
460 }
461 glob
462}
463
464fn push_glob_literal(glob: &mut String, text: &str) {
466 for character in text.chars() {
467 match character {
468 '*' | '?' | '[' => {
469 glob.push('[');
470 glob.push(character);
471 glob.push(']');
472 }
473 _ => glob.push(character),
474 }
475 }
476}
477
478fn key_matches(key: &str, positions: &[&[String]]) -> bool {
480 let mut syllables = key.split(' ');
481 positions.iter().all(|readings| {
482 syllables
483 .next()
484 .is_some_and(|syllable| readings.iter().any(|reading| reading == syllable))
485 }) && syllables.next().is_none()
486}
487
488fn glob_pattern(pattern: &str, wildcard: char, completions: bool) -> String {
490 let mut glob = String::with_capacity(pattern.len() * 4 + 1);
491 for character in pattern.chars() {
492 match character {
493 _ if character == wildcard => glob.push_str("[^ ]"),
494 '*' | '?' | '[' => {
495 glob.push('[');
496 glob.push(character);
497 glob.push(']');
498 }
499 _ => glob.push(character),
500 }
501 }
502 if completions {
503 glob.push('*');
504 }
505 glob
506}
507
508fn read_keyed_rows_into(
509 rows: &mut rusqlite::Rows<'_>,
510 result: &mut Vec<(String, LanguageEntry)>,
511 capacity: Option<usize>,
512) -> rusqlite::Result<()> {
513 let mut length = 0;
514 while let Some(row) = rows.next()? {
515 let key = row.get_ref(0)?.as_str().map_err(|error| {
516 rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
517 })?;
518 let text = row.get_ref(1)?.as_str().map_err(|error| {
519 rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error))
520 })?;
521 let weight = row.get(2)?;
522 reserve_query_capacity(result, length, capacity);
523 if let Some((existing_key, entry)) = result.get_mut(length) {
524 existing_key.clear();
525 existing_key.push_str(key);
526 entry.text.clear();
527 entry.text.push_str(text);
528 entry.weight = weight;
529 } else {
530 result.push((
531 key.to_owned(),
532 LanguageEntry {
533 text: text.to_owned(),
534 weight,
535 },
536 ));
537 }
538 length += 1;
539 }
540 result.truncate(length);
541 Ok(())
542}
543
544fn reserve_query_capacity<T>(result: &mut Vec<T>, length: usize, capacity: Option<usize>) {
545 if length == 0 && result.is_empty() && result.capacity() == 0 {
546 if let Some(capacity) = capacity {
547 result.reserve_exact(capacity);
548 }
549 }
550}
551
552fn completion_upper_bound(prefix: &str) -> Option<String> {
553 let last = prefix.chars().next_back()?;
554 let next = char::from_u32(u32::from(last) + 1)?;
555 let head = &prefix[..prefix.len() - last.len_utf8()];
556 let mut upper = String::with_capacity(head.len() + next.len_utf8());
557 upper.push_str(head);
558 upper.push(next);
559 Some(upper)
560}
561
562fn query_capacity(limit: usize) -> Option<usize> {
563 (limit < i32::MAX as usize).then_some(limit)
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 fn build(path: &Path, format_version: &str) {
571 let connection = Connection::open(path).unwrap();
572 connection.execute_batch(SCHEMA).unwrap();
573 for (name, value) in [
574 (METADATA_FORMAT_VERSION, format_version),
575 (
576 METADATA_SOURCE_COMMIT,
577 "259f0e48bba840c3a2e0d117539e96937f3d89bc",
578 ),
579 (METADATA_LICENSE, "CC-BY-4.0"),
580 ] {
581 connection
582 .execute("INSERT INTO metadata VALUES (?1, ?2)", (name, value))
583 .unwrap();
584 }
585 for syllable in ["nei", "hou"] {
586 connection
587 .execute("INSERT INTO syllables VALUES (?1)", (syllable,))
588 .unwrap();
589 }
590 for (key, text, weight) in [
591 ("nei hou", "你好", 900),
592 ("nei hou", "妳好", 40),
593 ("nei hou", "你號", 40),
594 ("nei", "你", 5000),
595 ] {
596 connection
597 .execute(
598 "INSERT INTO entries VALUES (?1, ?2, ?3)",
599 (key, text, weight),
600 )
601 .unwrap();
602 }
603 }
604
605 fn message(error: EngineError) -> String {
606 error.to_string()
607 }
608
609 #[test]
610 fn completion_upper_bound_increments_only_the_last_character() {
611 assert_eq!(completion_upper_bound("nei h").as_deref(), Some("nei i"));
612 assert_eq!(completion_upper_bound("ㄋㄧˇ").as_deref(), Some("ㄋㄧˈ"));
613 }
614
615 #[test]
616 fn browse_pages_all_rows_and_filters_literal_keys_and_words_without_writes() {
617 let dir = tempfile::tempdir().unwrap();
618 let path = dir.path().join("dictionary.db");
619 build(&path, &FORMAT_VERSION.to_string());
620 Connection::open(&path)
621 .unwrap()
622 .execute_batch("INSERT INTO entries VALUES('ㄋㄧˇ', '你%_', 10);")
623 .unwrap();
624 let before = std::fs::read(&path).unwrap();
625 let dictionary = open_read_only(&path).unwrap();
626 let all = dictionary.browse("", 0, 20).unwrap();
627 assert_eq!(all.len(), 5);
628 assert_eq!(dictionary.browse("", 2, 2).unwrap(), all[2..4]);
629 assert_eq!(dictionary.browse("NEI", 0, 10).unwrap().len(), 4);
630 assert_eq!(dictionary.browse("好", 0, 10).unwrap().len(), 2);
631 assert_eq!(dictionary.browse("ㄋ", 0, 10).unwrap()[0].value, "你%_");
632 assert_eq!(dictionary.browse("%_", 0, 10).unwrap()[0].value, "你%_");
633 assert!(dictionary.browse("不存在", 0, 10).unwrap().is_empty());
634 assert_eq!(std::fs::read(&path).unwrap(), before);
635 }
636
637 #[test]
638 fn round_trips_entries_syllables_and_metadata() {
639 let dir = tempfile::tempdir().unwrap();
640 let path = dir.path().join("msime-cantonese.db");
641 build(&path, &FORMAT_VERSION.to_string());
642
643 let dictionary = open_read_only(&path).unwrap();
644 let entry = |text: &str, weight| LanguageEntry {
645 text: text.to_owned(),
646 weight,
647 };
648 let entries = dictionary.lookup("nei hou", 10).unwrap();
649 assert_eq!(entries.capacity(), 10);
650 assert_eq!(
651 entries,
652 vec![entry("你好", 900), entry("你號", 40), entry("妳好", 40)]
653 );
654 assert_eq!(
655 dictionary.lookup("nei hou", 1).unwrap(),
656 vec![entry("你好", 900)]
657 );
658 assert_eq!(
659 dictionary.lookup("nei", 10).unwrap(),
660 vec![entry("你", 5000)]
661 );
662 let empty = dictionary.lookup("ngo", 10).unwrap();
663 assert!(empty.is_empty());
664 assert_eq!(empty.capacity(), 0);
665 assert!(dictionary.has_syllable("hou").unwrap());
666 assert!(!dictionary.has_syllable("ho").unwrap());
667 assert_eq!(
668 dictionary.metadata(METADATA_LICENSE).unwrap().as_deref(),
669 Some("CC-BY-4.0")
670 );
671 assert_eq!(dictionary.metadata("missing").unwrap(), None);
672 }
673
674 #[test]
675 fn lists_syllables_and_completes_the_last_syllable() {
676 let dir = tempfile::tempdir().unwrap();
677 let path = dir.path().join("msime-cantonese.db");
678 build(&path, &FORMAT_VERSION.to_string());
679 let connection = Connection::open(&path).unwrap();
680 for (key, text, weight) in [("nei hou aa", "你好呀", 10), ("nei i", "你意", 5)] {
681 connection
682 .execute(
683 "INSERT INTO entries VALUES (?1, ?2, ?3)",
684 (key, text, weight),
685 )
686 .unwrap();
687 }
688 drop(connection);
689
690 let dictionary = open_read_only(&path).unwrap();
691 let mut syllables = dictionary.syllables().unwrap();
692 syllables.sort();
693 assert_eq!(syllables, ["hou", "nei"]);
694 let completions = |prefix: &str, limit| {
695 let rows = dictionary.lookup_completions(prefix, limit).unwrap();
696 (
697 rows.capacity(),
698 rows.into_iter()
699 .map(|(key, entry)| (key, entry.text))
700 .collect::<Vec<_>>(),
701 )
702 };
703 let pair = |key: &str, text: &str| (key.to_owned(), text.to_owned());
704 let (capacity, rows) = completions("nei h", 10);
705 assert_eq!(capacity, 10);
706 assert_eq!(
707 rows,
708 [
709 pair("nei hou", "你好"),
710 pair("nei hou", "你號"),
711 pair("nei hou", "妳好")
712 ]
713 );
714 assert_eq!(completions("nei h", 1).1, [pair("nei hou", "你好")]);
715 assert_eq!(completions("ne", 10).1, [pair("nei", "你")]);
716 assert!(completions("nei ho", 0).1.is_empty());
717 let (capacity, rows) = completions("ngo", 10);
718 assert!(rows.is_empty());
719 assert_eq!(capacity, 0);
720 let (capacity, rows) = completions("", 10);
721 assert!(rows.is_empty());
722 assert_eq!(capacity, 0);
723 }
724
725 #[test]
726 fn glob_pattern_maps_the_wildcard_and_escapes_glob_metacharacters() {
727 assert_eq!(glob_pattern("hxs", 'x', false), "h[^ ]s");
728 assert_eq!(glob_pattern("hxs", 'x', true), "h[^ ]s*");
729 assert_eq!(glob_pattern("a*?[", 'x', false), "a[*][?][[]");
730 }
731
732 #[test]
734 fn matches_wildcard_patterns_within_the_literal_prefix() {
735 let dir = tempfile::tempdir().unwrap();
736 let path = dir.path().join("msime-stroke.db");
737 build(&path, &FORMAT_VERSION.to_string());
738 let connection = Connection::open(&path).unwrap();
739 for (key, text, weight) in [
740 ("hs", "十", 800),
741 ("hh", "二", 900),
742 ("hhh", "三", 700),
743 ("hsh", "土", 600),
744 ("hpn", "大", 950),
745 ("sh", "上", 650),
746 ("hs hs", "十十", 5000),
747 ("a*b", "星", 1),
748 ] {
749 connection
750 .execute(
751 "INSERT INTO entries VALUES (?1, ?2, ?3)",
752 (key, text, weight),
753 )
754 .unwrap();
755 }
756 drop(connection);
757
758 let dictionary = open_read_only(&path).unwrap();
759 let texts = |pattern: &str, completions, limit| {
760 let rows = dictionary
761 .lookup_pattern(pattern, 'x', completions, limit)
762 .unwrap();
763 let mut reused = Vec::new();
764 dictionary
765 .lookup_pattern_into(pattern, 'x', completions, limit, &mut reused)
766 .unwrap();
767 assert_eq!(reused, rows);
768 (
769 rows.capacity(),
770 rows.into_iter()
771 .map(|(key, entry)| format!("{key}:{}", entry.text))
772 .collect::<Vec<_>>(),
773 )
774 };
775 let (capacity, rows) = texts("hx", false, 10);
776 assert_eq!(capacity, 10);
777 assert_eq!(rows, ["hh:二", "hs:十"]);
778 assert_eq!(
779 texts("hx", true, 10).1,
780 ["hpn:大", "hh:二", "hs:十", "hhh:三", "hsh:土"]
781 );
782 assert_eq!(texts("hx", true, 2).1, ["hpn:大", "hh:二"]);
783 assert_eq!(texts("hxh", false, 10).1, ["hhh:三", "hsh:土"]);
784 assert_eq!(texts("xh", false, 10).1, ["hh:二", "sh:上"]);
785 assert_eq!(
786 texts("xxx", false, 10).1,
787 ["nei:你", "hpn:大", "hhh:三", "hsh:土", "a*b:星"]
788 );
789 assert_eq!(texts("hs", false, 10).1, ["hs:十"]);
790 assert_eq!(texts("hsx", true, 10).1, ["hsh:土"]);
791 assert_eq!(texts("a*b", false, 10).1, ["a*b:星"]);
793 let (capacity, rows) = texts("a?b", false, 10);
794 assert!(rows.is_empty());
795 assert_eq!(capacity, 0);
796 assert!(texts("", true, 10).1.is_empty());
797 let (capacity, rows) = texts("xxxxxx", true, 10);
798 assert!(rows.is_empty());
799 assert_eq!(capacity, 0);
800 assert!(texts("hx", true, 0).1.is_empty());
801 }
802
803 #[test]
804 fn wildcard_query_reuses_row_strings_and_truncates_stale_results() {
805 let dir = tempfile::tempdir().unwrap();
806 let path = dir.path().join("msime-stroke.db");
807 build(&path, &FORMAT_VERSION.to_string());
808 let dictionary = open_read_only(&path).unwrap();
809 let mut result = Vec::new();
810 for pattern in ["nei xxx", "xxx xxx"] {
811 dictionary
812 .lookup_pattern_into(pattern, 'x', false, 10, &mut result)
813 .unwrap();
814 assert_eq!(result.len(), 3);
815 let expected = result.clone();
816 let pointers = result
817 .iter()
818 .map(|(key, entry)| (key.as_ptr(), entry.text.as_ptr()))
819 .collect::<Vec<_>>();
820 let vector = result.as_ptr();
821 for (key, entry) in &mut result {
822 key.clear();
823 entry.text.clear();
824 entry.weight = -9;
825 }
826 dictionary
827 .lookup_pattern_into(pattern, 'x', false, 10, &mut result)
828 .unwrap();
829 assert_eq!(result, expected);
830 assert_eq!(result.as_ptr(), vector);
831 assert_eq!(
832 result
833 .iter()
834 .map(|(key, entry)| (key.as_ptr(), entry.text.as_ptr()))
835 .collect::<Vec<_>>(),
836 pointers
837 );
838 dictionary
839 .lookup_pattern_into(pattern, 'x', false, 1, &mut result)
840 .unwrap();
841 assert_eq!(result, expected[..1]);
842 }
843 for (pattern, limit) in [("xxx xxx", 0), ("missing", 10), ("", 10)] {
844 dictionary
845 .lookup_pattern_into("nei xxx", 'x', false, 10, &mut result)
846 .unwrap();
847 assert!(!result.is_empty());
848 dictionary
849 .lookup_pattern_into(pattern, 'x', false, limit, &mut result)
850 .unwrap();
851 assert!(result.is_empty());
852 }
853 }
854
855 fn readings(values: &[&str]) -> Vec<String> {
856 values.iter().map(|value| (*value).to_owned()).collect()
857 }
858
859 #[test]
861 fn looks_up_keys_whose_syllables_fall_in_each_position() {
862 let dir = tempfile::tempdir().unwrap();
863 let path = dir.path().join("msime-zhuyin.db");
864 build(&path, &FORMAT_VERSION.to_string());
865 let connection = Connection::open(&path).unwrap();
866 for (key, text, weight) in [
867 ("ㄋㄧˇ", "你", 1000),
868 ("ㄋㄧˇ", "妳", 300),
869 ("ㄌㄧˇ", "李", 1200),
870 ("ㄌㄧˇ", "你", 5),
871 ("ㄋㄧˇ ㄏㄠˇ", "你好", 500),
872 ("ㄌㄧˇ ㄏㄠˇ", "李好", 500),
873 ("ㄋㄧˇ ㄏㄠˋ", "你號", 900),
874 ("ㄋㄧ ㄏㄠˇ", "妮好", 900),
875 ("ㄋㄧˇ ㄏㄠˇ ㄇㄚ˙", "你好嗎", 900),
876 ("ㄋㄧˇ ㄏㄨㄚˇ", "你畫", 900),
877 ("ㄌㄧˇ ㄎㄠˇ", "李考", 400),
878 ("ㄋㄧˇ ㄍㄠˇ", "你搞", 700),
879 ] {
880 connection
881 .execute(
882 "INSERT INTO entries VALUES (?1, ?2, ?3)",
883 (key, text, weight),
884 )
885 .unwrap();
886 }
887 drop(connection);
888 let dictionary = open_read_only(&path).unwrap();
889 let ni_li = readings(&["ㄋㄧˇ", "ㄌㄧˇ"]);
890 let hao = readings(&["ㄏㄠˇ"]);
891 let gao_kao_hao = readings(&["ㄍㄠˇ", "ㄎㄠˇ", "ㄏㄠˇ"]);
892 let rows = |positions: &[&[String]], limit| {
893 dictionary
894 .lookup_readings(positions, limit)
895 .unwrap()
896 .into_iter()
897 .map(|(key, entry)| format!("{key}:{}:{}", entry.text, entry.weight))
898 .collect::<Vec<_>>()
899 };
900
901 assert_eq!(
903 rows(&[&ni_li, &hao], usize::MAX),
904 ["ㄋㄧˇ ㄏㄠˇ:你好:500", "ㄌㄧˇ ㄏㄠˇ:李好:500"]
905 );
906 assert_eq!(
908 rows(&[&ni_li, &gao_kao_hao], usize::MAX),
909 [
910 "ㄋㄧˇ ㄍㄠˇ:你搞:700",
911 "ㄋㄧˇ ㄏㄠˇ:你好:500",
912 "ㄌㄧˇ ㄏㄠˇ:李好:500",
913 "ㄌㄧˇ ㄎㄠˇ:李考:400",
914 ]
915 );
916 assert_eq!(rows(&[&ni_li, &gao_kao_hao], 1), ["ㄋㄧˇ ㄍㄠˇ:你搞:700"]);
917 assert_eq!(
919 rows(&[&ni_li], usize::MAX),
920 [
921 "ㄌㄧˇ:李:1200",
922 "ㄋㄧˇ:你:1000",
923 "ㄋㄧˇ:妳:300",
924 "ㄌㄧˇ:你:5"
925 ]
926 );
927 assert_eq!(rows(&[&ni_li], 2), ["ㄌㄧˇ:李:1200", "ㄋㄧˇ:你:1000"]);
928 let ni = readings(&["ㄋㄧˇ"]);
930 let expected: Vec<String> = dictionary
931 .lookup("ㄋㄧˇ ㄏㄠˇ", usize::MAX)
932 .unwrap()
933 .into_iter()
934 .map(|entry| format!("ㄋㄧˇ ㄏㄠˇ:{}:{}", entry.text, entry.weight))
935 .collect();
936 assert_eq!(rows(&[&ni, &hao], usize::MAX), expected);
937 assert_eq!(rows(&[&ni], 1), ["ㄋㄧˇ:你:1000"]);
938 assert!(rows(&[], 10).is_empty());
939 assert!(rows(&[&hao, &ni_li], 10).is_empty());
940 }
941
942 #[test]
943 fn single_reading_result_does_not_clone_the_query_key() {
944 let dir = tempfile::tempdir().unwrap();
945 let path = dir.path().join("msime-zhuyin.db");
946 build(&path, &FORMAT_VERSION.to_string());
947 let connection = Connection::open(&path).unwrap();
948 connection
949 .execute(
950 "INSERT INTO entries VALUES (?1, ?2, ?3)",
951 ("ㄋㄧˇ", "你", 1000),
952 )
953 .unwrap();
954 drop(connection);
955
956 let dictionary = open_read_only(&path).unwrap();
957 let reading = readings(&["ㄋㄧˇ"]);
958 let expected = dictionary.lookup_readings(&[&reading], 1).unwrap();
960 let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
961 dictionary.lookup_readings(&[&reading], 1).unwrap()
962 });
963
964 assert_eq!(actual, expected);
965 assert_eq!(
966 allocations, 5,
967 "单结果查询不应为键再分配副本:{allocations}"
968 );
969 }
970
971 #[test]
972 fn alternative_readings_write_rows_without_temporary_result_vectors() {
973 let dir = tempfile::tempdir().unwrap();
974 let path = dir.path().join("msime-zhuyin.db");
975 build(&path, &FORMAT_VERSION.to_string());
976 let connection = Connection::open(&path).unwrap();
977 for (key, text, weight) in [
978 ("ㄋㄧˇ", "你", 1000),
979 ("ㄋㄧˇ", "妳", 900),
980 ("ㄌㄧˇ", "李", 800),
981 ("ㄌㄧˇ", "里", 700),
982 ] {
983 connection
984 .execute(
985 "INSERT INTO entries VALUES (?1, ?2, ?3)",
986 (key, text, weight),
987 )
988 .unwrap();
989 }
990 drop(connection);
991
992 let dictionary = open_read_only(&path).unwrap();
993 let readings = readings(&["ㄋㄧˇ", "ㄌㄧˇ"]);
994 let expected = dictionary.lookup_readings(&[&readings], 10).unwrap();
996 let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
997 dictionary.lookup_readings(&[&readings], 10).unwrap()
998 });
999
1000 assert_eq!(actual, expected);
1001 assert!(
1002 allocations <= 10,
1003 "多个读音查询不应为每个读音建立临时结果向量:{allocations}"
1004 );
1005 }
1006
1007 #[test]
1008 fn missing_alternative_readings_reuse_query_string_storage() {
1009 let dir = tempfile::tempdir().unwrap();
1010 let path = dir.path().join("msime-zhuyin.db");
1011 build(&path, &FORMAT_VERSION.to_string());
1012 let dictionary = open_read_only(&path).unwrap();
1013 let first = readings(&[
1014 "ㄅㄚ", "ㄆㄚ", "ㄇㄚ", "ㄈㄚ", "ㄉㄚ", "ㄊㄚ", "ㄋㄚ", "ㄌㄚ",
1015 ]);
1016 let rest = readings(&["ㄏㄠˇ"]);
1017 assert!(dictionary
1019 .lookup_readings(&[&first, &rest], usize::MAX)
1020 .unwrap()
1021 .is_empty());
1022 let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
1023 dictionary
1024 .lookup_readings(&[&first, &rest], usize::MAX)
1025 .unwrap()
1026 });
1027 assert!(actual.is_empty());
1028 assert_eq!(actual.capacity(), 0);
1029 assert!(
1030 allocations <= 8,
1031 "多个首读音不应各自分配查询字符串:{allocations}"
1032 );
1033 }
1034
1035 #[test]
1036 fn reused_reading_query_strings_keep_literal_keys_order_and_limits() {
1037 let dir = tempfile::tempdir().unwrap();
1038 let path = dir.path().join("msime-zhuyin.db");
1039 build(&path, &FORMAT_VERSION.to_string());
1040 let connection = Connection::open(&path).unwrap();
1041 for (key, text, weight) in [
1042 ("a tail", "row-a", 5),
1043 ("b tale", "row-b", 90),
1044 ("synthetic*?[ tail", "same", 70),
1045 ("ㄋㄧˇ tail", "same", 70),
1046 ("syntheticXX[ tail", "wrong-literal", 1000),
1047 ("a taie", "wrong-reading", 1000),
1048 ("a tail extra", "wrong-span", 1000),
1049 ] {
1050 connection
1051 .execute(
1052 "INSERT INTO entries VALUES (?1, ?2, ?3)",
1053 (key, text, weight),
1054 )
1055 .unwrap();
1056 }
1057 drop(connection);
1058 let dictionary = open_read_only(&path).unwrap();
1059 let first = readings(&["a", "ㄋㄧˇ", "a", "synthetic*?[", "b"]);
1061 let rest = readings(&["tail", "tale"]);
1062 let expected = [
1063 ("b tale", "row-b", 90),
1064 ("synthetic*?[ tail", "same", 70),
1065 ("ㄋㄧˇ tail", "same", 70),
1066 ("a tail", "row-a", 5),
1067 ("a tail", "row-a", 5),
1068 ]
1069 .map(|(key, text, weight)| {
1070 (
1071 key.to_owned(),
1072 LanguageEntry {
1073 text: text.to_owned(),
1074 weight,
1075 },
1076 )
1077 });
1078 for limit in [0, 1, 3, usize::MAX] {
1079 let actual = dictionary.lookup_readings(&[&first, &rest], limit).unwrap();
1080 assert_eq!(actual, expected[..limit.min(expected.len())]);
1081 }
1082 let actual = dictionary
1083 .lookup_readings(&[&first, &rest], usize::MAX)
1084 .unwrap();
1085 drop(first);
1086 drop(rest);
1087 drop(dictionary);
1088 assert_eq!(actual, expected);
1089 }
1090
1091 #[test]
1092 fn reading_glob_rejects_rows_before_allocating_their_keys() {
1093 let dir = tempfile::tempdir().unwrap();
1094 let path = dir.path().join("msime-zhuyin.db");
1095 build(&path, &FORMAT_VERSION.to_string());
1096 let connection = Connection::open(&path).unwrap();
1097 for (key, text, weight) in [
1098 ("ㄋㄧˇ ㄏㄠˇ", "你好", 1000),
1099 ("ㄋㄧˇ ㄏㄠˇ ㄇㄚ˙", "你好嗎", 900),
1100 ("ㄋㄧˇ ㄏㄠˇ ㄉㄜ˙", "你的", 800),
1101 ] {
1102 connection
1103 .execute(
1104 "INSERT INTO entries VALUES (?1, ?2, ?3)",
1105 (key, text, weight),
1106 )
1107 .unwrap();
1108 }
1109 drop(connection);
1110
1111 let dictionary = open_read_only(&path).unwrap();
1112 let first = readings(&["ㄋㄧˇ"]);
1113 let rest = readings(&["ㄏㄠˇ", "ㄏㄠˇ ㄇㄚ˙"]);
1114 let expected = dictionary
1115 .lookup_readings(&[&first, &rest], usize::MAX)
1116 .unwrap();
1117 let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
1118 dictionary
1119 .lookup_readings(&[&first, &rest], usize::MAX)
1120 .unwrap()
1121 });
1122
1123 assert_eq!(actual, expected);
1124 assert!(
1125 allocations <= 11,
1126 "粗筛淘汰行不应分配键字符串:{allocations}"
1127 );
1128 }
1129
1130 #[test]
1131 fn multiple_reading_results_clone_the_query_key_only_after_the_first_row() {
1132 let dir = tempfile::tempdir().unwrap();
1133 let path = dir.path().join("msime-zhuyin.db");
1134 build(&path, &FORMAT_VERSION.to_string());
1135 let connection = Connection::open(&path).unwrap();
1136 for (text, weight) in [("你", 1000), ("妳", 900), ("尼", 800)] {
1137 connection
1138 .execute(
1139 "INSERT INTO entries VALUES (?1, ?2, ?3)",
1140 ("ㄋㄧˇ", text, weight),
1141 )
1142 .unwrap();
1143 }
1144 drop(connection);
1145
1146 let dictionary = open_read_only(&path).unwrap();
1147 let reading = readings(&["ㄋㄧˇ"]);
1148 let expected = dictionary.lookup_readings(&[&reading], 10).unwrap();
1150 let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
1151 dictionary.lookup_readings(&[&reading], 10).unwrap()
1152 });
1153
1154 assert_eq!(actual, expected);
1155 assert_eq!(
1156 allocations, 9,
1157 "多结果查询的第一行应直接取得原键:{allocations}"
1158 );
1159 }
1160
1161 #[test]
1162 fn joins_single_readings_in_order() {
1163 let ni = readings(&["ㄋㄧˇ"]);
1164 let hao = readings(&["ㄏㄠˇ"]);
1165 assert_eq!(join_single_readings(&[&ni, &hao]), "ㄋㄧˇ ㄏㄠˇ");
1166 }
1167
1168 #[test]
1169 fn readings_glob_writes_literals_classes_and_wildcards() {
1170 let ni_li = readings(&["ㄋㄧˇ", "ㄌㄧˇ"]);
1171 let hao = readings(&["ㄏㄠˇ"]);
1172 let uneven = readings(&["ㄏㄠˇ", "ㄏㄨㄚˇ"]);
1173 let meta = readings(&["a*", "b-"]);
1174 assert_eq!(readings_glob(&[&hao]), "ㄏㄠˇ");
1175 assert_eq!(readings_glob(&[&hao, &ni_li]), "ㄏㄠˇ [ㄋㄌ]ㄧˇ");
1176 assert_eq!(readings_glob(&[&uneven, &hao]), "* ㄏㄠˇ");
1177 assert_eq!(readings_glob(&[&meta]), "[ab]?");
1178 assert_eq!(readings_glob(&[&readings(&["a?"])]), "a[?]");
1179 assert!(key_matches("ㄋㄧˇ ㄏㄠˇ", &[&ni_li, &hao]));
1180 assert!(!key_matches("ㄋㄧˇ", &[&ni_li, &hao]));
1181 assert!(!key_matches("ㄋㄧˇ ㄏㄠˇ ㄏㄠˇ", &[&ni_li, &hao]));
1182 }
1183
1184 #[cfg(unix)]
1185 #[test]
1186 fn opens_through_a_symlinked_directory() {
1187 let dir = tempfile::tempdir().unwrap();
1188 let real = dir.path().join("real");
1189 std::fs::create_dir(&real).unwrap();
1190 build(&real.join("msime-zhuyin.db"), &FORMAT_VERSION.to_string());
1191 let link = dir.path().join("link");
1192 std::os::unix::fs::symlink(&real, &link).unwrap();
1193
1194 let dictionary = open_read_only(&link.join("msime-zhuyin.db")).unwrap();
1195 assert!(dictionary.has_syllable("nei").unwrap());
1196 }
1197
1198 #[test]
1199 fn refuses_an_unknown_format_version() {
1200 let dir = tempfile::tempdir().unwrap();
1201 let path = dir.path().join("msime-zhuyin.db");
1202 build(&path, "2");
1203 assert_eq!(
1204 message(open_read_only(&path).err().unwrap()),
1205 diagnostics::LANGUAGE_DICTIONARY_VERSION_UNSUPPORTED
1206 );
1207 }
1208
1209 #[test]
1210 fn refuses_missing_empty_and_foreign_files_without_creating_them() {
1211 let dir = tempfile::tempdir().unwrap();
1212 let missing = dir.path().join("msime-cantonese.db");
1213 assert_eq!(
1214 message(open_read_only(&missing).err().unwrap()),
1215 diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE
1216 );
1217 assert!(!missing.exists());
1218 assert_eq!(
1219 message(open_read_only(Path::new("")).err().unwrap()),
1220 diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE
1221 );
1222 let foreign = dir.path().join("foreign.db");
1223 std::fs::write(&foreign, b"not a database").unwrap();
1224 assert_eq!(
1225 message(open_read_only(&foreign).err().unwrap()),
1226 diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE
1227 );
1228 let no_metadata = dir.path().join("no_metadata.db");
1229 Connection::open(&no_metadata)
1230 .unwrap()
1231 .execute_batch("CREATE TABLE other(x)")
1232 .unwrap();
1233 assert_eq!(
1234 message(open_read_only(&no_metadata).err().unwrap()),
1235 diagnostics::LANGUAGE_DICTIONARY_UNAVAILABLE
1236 );
1237 }
1238}