use super::*;
use crate::dictionary::fixtures::english_db;
#[test]
fn missing_prefix_keeps_no_candidate_storage() {
let directory = tempfile::tempdir().unwrap();
let path = english_db(directory.path(), &[("alpha", "Alpha", 10)], &[], &[]);
let dictionary = EnglishDictionary::open(&path, None, None);
let result = dictionary.query_prefix("missing", 1000);
assert!(result.is_empty());
assert_eq!(result.capacity(), 0);
}
fn original_query(dictionary: &EnglishDictionary, prefix: &str, limit: usize) -> Vec<WordItem> {
let Some(connection) = &dictionary.connection else {
return Vec::new();
};
if !is_lower_ascii_word(prefix) || limit == 0 {
return Vec::new();
}
let upper_bound = prefix_upper_bound(prefix);
let Ok(mut statement) = connection.prepare_cached(&prefix_sql(sql_limit(limit))) else {
return Vec::new();
};
let Ok(mut rows) = statement.query([prefix, upper_bound.as_str()]) else {
return Vec::new();
};
let mut candidates = Vec::with_capacity(limit);
loop {
match rows.next() {
Ok(Some(row)) => {
let is_null = |index| matches!(row.get_ref(index), Ok(ValueRef::Null));
if is_null(0) || is_null(1) {
continue;
}
let (Ok(word), Ok(display), Ok(weight)) =
(column_text(row, 0), column_text(row, 1), column_i64(row, 2))
else {
return Vec::new();
};
candidates.push(WordItem::new(
word,
display,
weight,
CandidateSource::EnglishDictionary,
"",
));
}
Ok(None) => return candidates,
Err(_) => return Vec::new(),
}
}
}
use crate::ime::personal_rerank::allocations::{count, measure};
fn compare_hot(dictionary: &EnglishDictionary, prefix: &str, limit: usize, saved: usize) {
drop(original_query(dictionary, prefix, limit));
drop(dictionary.query_prefix(prefix, limit));
let (old, old_count) = count(|| original_query(dictionary, prefix, limit));
let (new, new_count) = count(|| dictionary.query_prefix(prefix, limit));
assert_eq!(new, old, "prefix={prefix:?}, limit={limit}");
assert_eq!(
new_count + saved,
old_count,
"prefix={prefix:?}, limit={limit}"
);
if !new.is_empty() {
assert_eq!(new.capacity(), old.capacity());
} else {
assert_eq!(new.capacity(), 0);
}
}
#[test]
fn hits_preserve_every_field_order_capacity_and_allocation_count() {
let directory = tempfile::tempdir().unwrap();
let path = english_db(
directory.path(),
&[
("alpha", "Alpha", -2),
("alpha", "Alpha alternate", -3),
("alphabet", "Alphabet", i64::from(i32::MAX) + 100),
("alpine", "Alpine", 20),
("alps", "Alps B", 20),
("alps", "Alps A", 20),
("aloft", "Aloft", 20),
],
&[],
&[],
);
let dictionary = EnglishDictionary::open(&path, None, None);
for prefix in ["alpha", "al", "alps"] {
for limit in [1, 2, 5, 1000] {
compare_hot(&dictionary, prefix, limit, 0);
}
}
let result = dictionary.query_prefix("al", 1000);
assert_eq!(
result
.iter()
.map(|item| item.word.as_str())
.collect::<Vec<_>>(),
[
"Alphabet",
"Alps A",
"Alps B",
"Aloft",
"Alpine",
"Alpha",
"Alpha alternate"
]
);
assert_eq!(dictionary.query_prefix("alpha", 1)[0].word, "Alpha");
for limit in [1, 5, 1000] {
compare_hot(&dictionary, "missing", limit, 1);
}
}
#[test]
fn null_rows_keep_sql_limit_and_allocate_only_for_valid_rows() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("nullable.db");
let connection = Connection::open(&path).unwrap();
connection
.execute_batch(
"CREATE TABLE english_words(word TEXT,display TEXT,weight INTEGER);
INSERT INTO english_words VALUES('alpha',NULL,100),('alphabet','Alphabet',20),
('alpine','Alpine',NULL),('nullword',NULL,1),(NULL,'Ignored',1);",
)
.unwrap();
drop(connection);
let dictionary = EnglishDictionary::open(&path, None, None);
compare_hot(&dictionary, "alpha", 1, 1);
assert!(dictionary.query_prefix("alpha", 1).is_empty());
compare_hot(&dictionary, "alpha", 2, 0);
assert_eq!(dictionary.query_prefix("alpha", 2)[0].word, "Alphabet");
compare_hot(&dictionary, "al", 1000, 0);
assert_eq!(dictionary.query_prefix("alpine", 5)[0].weight, 0);
compare_hot(&dictionary, "nullword", 1000, 1);
}
#[test]
fn invalid_missing_and_prepare_failure_do_not_reach_the_reservation() {
let dictionary = EnglishDictionary::open(Path::new(""), None, None);
compare_hot(&dictionary, "alpha", 1000, 0);
let directory = tempfile::tempdir().unwrap();
let path = english_db(directory.path(), &[("alpha", "Alpha", 10)], &[], &[]);
let dictionary = EnglishDictionary::open(&path, None, None);
for prefix in ["", "Alpha", "al1", "合成"] {
compare_hot(&dictionary, prefix, 1000, 0);
}
compare_hot(&dictionary, "alpha", 0, 0);
Connection::open(&path)
.unwrap()
.execute_batch("DROP TABLE english_words")
.unwrap();
let dictionary = EnglishDictionary {
connection: Some(Connection::open(&path).unwrap()),
..dictionary
};
assert!(dictionary
.connection
.as_ref()
.unwrap()
.prepare_cached(&prefix_sql(1000))
.is_err());
compare_hot(&dictionary, "alpha", 1000, 0);
}
#[test]
fn full_pages_keep_a_single_candidate_reservation() {
let directory = tempfile::tempdir().unwrap();
let path = english_db(directory.path(), &[], &[], &[]);
let mut connection = Connection::open(&path).unwrap();
let transaction = connection.transaction().unwrap();
for index in 0..1024 {
let suffix = [
b'a' + (index / 676) as u8,
b'a' + ((index / 26) % 26) as u8,
b'a' + (index % 26) as u8,
];
let word = format!("al{}", std::str::from_utf8(&suffix).unwrap());
transaction
.execute(
"INSERT INTO english_words VALUES(?1,?1,?2)",
(&word, index as i64),
)
.unwrap();
}
transaction.commit().unwrap();
drop(connection);
let dictionary = EnglishDictionary::open(&path, None, None);
for limit in [1, 5, 1000] {
compare_hot(&dictionary, "al", limit, 0);
assert_eq!(dictionary.query_prefix("al", limit).len(), limit);
}
}
#[test]
fn first_step_failure_saves_the_unused_reservation() {
let directory = tempfile::tempdir().unwrap();
let path = english_db(directory.path(), &[("alpha", "Alpha", 10)], &[], &[]);
let dictionary = EnglishDictionary::open(&path, None, None);
compare_hot(&dictionary, "alpha", 1000, 0);
let writer = Connection::open(&path).unwrap();
writer.execute_batch("BEGIN EXCLUSIVE").unwrap();
{
let mut statement = dictionary
.connection
.as_ref()
.unwrap()
.prepare_cached(&prefix_sql(1000))
.unwrap();
let mut rows = statement.query(["alpha", "alpha{"]).unwrap();
assert!(rows.next().is_err());
}
compare_hot(&dictionary, "alpha", 1000, 1);
writer.execute_batch("ROLLBACK").unwrap();
compare_hot(&dictionary, "alpha", 1000, 0);
}
#[test]
fn cold_empty_query_removes_limit_sized_peak_and_returned_storage() {
let directory = tempfile::tempdir().unwrap();
let path = english_db(directory.path(), &[("alpha", "Alpha", 10)], &[], &[]);
let cold = |limit, original| {
measure(|| {
let dictionary = EnglishDictionary::open(&path, None, None);
let result = if original {
original_query(&dictionary, "missing", limit)
} else {
dictionary.query_prefix("missing", limit)
};
drop(dictionary);
result
})
};
let (old, old_heap) = cold(1000, true);
let (new, new_heap) = cold(1000, false);
assert_eq!(new, old);
assert_eq!(new.capacity(), 0);
assert_eq!(old_heap.minimum_bytes, 0);
assert_eq!(new_heap.minimum_bytes, 0);
assert_eq!(new_heap.remaining_bytes, 0);
assert_eq!(
old_heap.remaining_bytes,
(old.capacity() * size_of::<WordItem>()) as i128
);
assert_eq!(new_heap.allocations + 1, old_heap.allocations);
assert!(old_heap.peak_bytes > new_heap.peak_bytes);
let (_, larger_heap) = cold(9999, false);
assert_eq!(larger_heap.minimum_bytes, 0);
assert_eq!(larger_heap.remaining_bytes, 0);
assert_eq!(larger_heap.peak_bytes, new_heap.peak_bytes);
eprintln!(
"WordItem={} old={old_heap:?} new={new_heap:?}",
size_of::<WordItem>()
);
}