feat(engine): 五笔前缀逐键提示与调频接线
- 码表查询从精确匹配改为前缀匹配:精确等长行优先(保住一级/二级简码), 前缀行按权重降序跟随,LIMIT 50 截断(引擎层不读 server 配置的分层边界) - WubiCandidateProvider 注入 RuntimePaths:接通 update_weight / delete_by_key 到 user_dictionary journal;新增 bump_wubi_weight(同码组 MAX+1、主库更新 与 journal upsert 同事务,行缺失不造行) - create_word / cache_dynamic_candidate 补「为何 no-op」注释(后续任务范畴) - 两个 toml 的 wubi_schema 注释与实现对齐:当前仅支持 wubi86 - server 测试:前缀语义、LIMIT 截断、上界构造、调频+journal 回放、删词墓碑
This commit is contained in:
1 parent
cdfad7c04c
commit
c6632e8d85
8 files changed
+301
-29
No files matched your search
@@ -3,7 +3,7 @@
|
||||
|
||||
ProviderRegistry::ProviderRegistry(const ShuangpinProfile &shuangpin_profile, metasequoia::RuntimePaths paths)
|
||||
: pinyin_provider_(shuangpin_profile, paths),
|
||||
wubi_provider_(metasequoia::path_to_utf8(paths.dictionary(metasequoia::assets::main_dictionary))),
|
||||
wubi_provider_(metasequoia::path_to_utf8(paths.dictionary(metasequoia::assets::main_dictionary)), paths),
|
||||
japanese_provider_(metasequoia::path_to_utf8(paths.dictionary(metasequoia::assets::main_dictionary)),
|
||||
metasequoia::path_to_utf8(paths.resource(metasequoia::assets::japanese_model)))
|
||||
{
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
#include "wubi_candidate_provider.h"
|
||||
#include "../contracts/assets/assets.h"
|
||||
#include "../core/data_path.h"
|
||||
#include "../quanpin/quanpin_query.h"
|
||||
#include "../user_dictionary/user_dictionary_journal.h"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kNoMutation = 0;
|
||||
}
|
||||
// 与 QuanpinDictionary::ERROR_CODE 同值:调用方(learn_candidate)丢弃返回值,这里只保持
|
||||
// 「成功 0 / 失败非 0」的既有约定。
|
||||
constexpr int kMutationFailed = -1;
|
||||
// 前缀查询截断上限:候选窗每页最多 10 个,50 给固定位置/重排留足余量。1 码前缀在真实
|
||||
// 词库上可命中数千行,必须截断;provider 在引擎层,不读 server 配置(分层边界)。
|
||||
constexpr int kWubiQueryRowLimit = 50;
|
||||
} // namespace
|
||||
|
||||
WubiCandidateProvider::WubiCandidateProvider(std::string db_path)
|
||||
: db_path_(db_path.empty() ? quanpin::get_default_db_path() : std::move(db_path))
|
||||
WubiCandidateProvider::WubiCandidateProvider(std::string db_path, metasequoia::RuntimePaths paths)
|
||||
: db_path_(db_path.empty() ? quanpin::get_default_db_path() : std::move(db_path)), paths_(std::move(paths))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -28,7 +37,12 @@ std::vector<WordItem> WubiCandidateProvider::query(const QueryRequest &request)
|
||||
|
||||
sqlite3_reset(query_statement_);
|
||||
sqlite3_clear_bindings(query_statement_);
|
||||
if (sqlite3_bind_text(query_statement_, 1, request.normalized_input.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
|
||||
// 上界 = 前缀末字节 +1:五笔码字母域是 a–y(z 被方案丢弃,规范化后只剩小写字母),
|
||||
// 末字节自增不会越过域边界,无需进位处理。
|
||||
std::string upper_bound = request.normalized_input;
|
||||
++upper_bound.back();
|
||||
if (sqlite3_bind_text(query_statement_, 1, request.normalized_input.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
||||
sqlite3_bind_text(query_statement_, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
@@ -61,28 +75,47 @@ void WubiCandidateProvider::reset_cache()
|
||||
|
||||
int WubiCandidateProvider::create_word(SchemeType, std::string, std::string)
|
||||
{
|
||||
// 在线造词是后续任务(PRD Non-goals);设置页词典管理已覆盖手工造词。这里保持 no-op,
|
||||
// 不伪造一条无法通过词组取码规则校验的插入。
|
||||
return kNoMutation;
|
||||
}
|
||||
|
||||
int WubiCandidateProvider::update_weight_by_pinyin_and_word(SchemeType, std::string, std::string)
|
||||
int WubiCandidateProvider::update_weight_by_pinyin_and_word(SchemeType, std::string code, std::string word)
|
||||
{
|
||||
// 增量语义对齐全拼侧 build_sql_for_updating_word:新权重 = 同码组当前最高权重 + 1,
|
||||
// 选中候选升到组内首位;主库更新与 journal upsert 同事务完成,升级回放可重建。
|
||||
if (!user_dictionary::bump_wubi_weight(db_path_, journal_db_path(), code, word))
|
||||
{
|
||||
return kMutationFailed;
|
||||
}
|
||||
// 权重参与 ORDER BY,丢弃查询连接让下次查询按新权重重新排序。
|
||||
reset_cache();
|
||||
return kNoMutation;
|
||||
}
|
||||
|
||||
int WubiCandidateProvider::delete_by_pinyin_and_word(SchemeType, std::string, std::string)
|
||||
int WubiCandidateProvider::delete_by_pinyin_and_word(SchemeType, std::string code, std::string word)
|
||||
{
|
||||
// user_dictionary 删词本就按 DictionaryKind 泛化,五笔此前只是没有接线。
|
||||
if (!user_dictionary::delete_dictionary_candidate(db_path_, journal_db_path(),
|
||||
user_dictionary::DictionaryKind::Wubi, code, word))
|
||||
{
|
||||
return kMutationFailed;
|
||||
}
|
||||
reset_cache();
|
||||
return kNoMutation;
|
||||
}
|
||||
|
||||
int WubiCandidateProvider::cache_dynamic_candidate(SchemeType, const std::string &, const std::string &,
|
||||
CandidateSource)
|
||||
{
|
||||
// 五笔候选全部来自码表,没有云/AI 动态候选可缓存;no-op 是终态而非缺口。
|
||||
return kNoMutation;
|
||||
}
|
||||
|
||||
int WubiCandidateProvider::cache_dynamic_candidate_for_request(const QueryRequest &, const std::string &,
|
||||
CandidateSource)
|
||||
{
|
||||
// 同 cache_dynamic_candidate:码表型方案没有动态候选来源。
|
||||
return kNoMutation;
|
||||
}
|
||||
|
||||
@@ -100,9 +133,15 @@ bool WubiCandidateProvider::ensure_query_statement()
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr const char *query_sql = "SELECT \"key\", \"value\", \"weight\" FROM wubi86 "
|
||||
"WHERE \"key\" = ?1 ORDER BY \"weight\" DESC, rowid ASC";
|
||||
if (sqlite3_prepare_v2(db_, query_sql, -1, &query_statement_, nullptr) != SQLITE_OK)
|
||||
const std::string query_sql =
|
||||
"SELECT \"key\", \"value\", \"weight\" FROM wubi86 "
|
||||
"WHERE \"key\" >= ?1 AND \"key\" < ?2 "
|
||||
// 精确等长行优先(保住一级/二级简码),其余前缀行按权重降序;rowid 做稳定序,与现状一致。
|
||||
// key >= ?1 AND key < ?2 是范围扫描,受益于 key 上任何索引;LIMIT 只在截断处付出排序代价。
|
||||
"ORDER BY (\"key\" = ?1) DESC, \"weight\" DESC, \"key\" ASC, rowid ASC "
|
||||
"LIMIT " +
|
||||
std::to_string(kWubiQueryRowLimit);
|
||||
if (sqlite3_prepare_v2(db_, query_sql.c_str(), -1, &query_statement_, nullptr) != SQLITE_OK)
|
||||
{
|
||||
(void)0;
|
||||
close_database();
|
||||
@@ -111,6 +150,11 @@ bool WubiCandidateProvider::ensure_query_statement()
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string WubiCandidateProvider::journal_db_path() const
|
||||
{
|
||||
return metasequoia::path_to_utf8(paths_.user(metasequoia::assets::user_journal));
|
||||
}
|
||||
|
||||
void WubiCandidateProvider::close_database()
|
||||
{
|
||||
if (query_statement_ != nullptr)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "candidate_provider.h"
|
||||
#include "../core/runtime_paths.h"
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
|
||||
class WubiCandidateProvider : public ICandidateProvider
|
||||
{
|
||||
public:
|
||||
explicit WubiCandidateProvider(std::string db_path = {});
|
||||
// paths 供调频/删词写 user journal 用;db_path 是测试注入口,生产路径由 ProviderRegistry
|
||||
// 按 paths 解析后传入,两者指向同一份主库。
|
||||
explicit WubiCandidateProvider(std::string db_path = {},
|
||||
metasequoia::RuntimePaths paths = metasequoia::RuntimePaths::legacy());
|
||||
~WubiCandidateProvider() override;
|
||||
|
||||
WubiCandidateProvider(const WubiCandidateProvider &) = delete;
|
||||
@@ -30,8 +34,10 @@ class WubiCandidateProvider : public ICandidateProvider
|
||||
private:
|
||||
bool ensure_query_statement();
|
||||
void close_database();
|
||||
std::string journal_db_path() const;
|
||||
|
||||
std::string db_path_;
|
||||
metasequoia::RuntimePaths paths_;
|
||||
sqlite3 *db_ = nullptr;
|
||||
sqlite3_stmt *query_statement_ = nullptr;
|
||||
};
|
||||
@@ -735,6 +735,54 @@ bool delete_english_candidate(const std::string &english_db_path, const std::str
|
||||
return delete_dictionary_candidate(english_db_path, user_db_path, DictionaryKind::English, entry_key, value);
|
||||
}
|
||||
|
||||
bool bump_wubi_weight(const std::string &main_db_path, const std::string &user_db_path, const std::string &key,
|
||||
const std::string &value)
|
||||
{
|
||||
if (key.empty() || value.empty())
|
||||
return false;
|
||||
// Initialize the journal before attaching it; every operation owns its connections.
|
||||
if (!ensure_user_database(user_db_path))
|
||||
return false;
|
||||
auto database = open_database(main_db_path, SQLITE_OPEN_READWRITE);
|
||||
auto attach = database ? prepare(database.get(), "ATTACH DATABASE ?1 AS candidate_journal") : Stmt{};
|
||||
if (!attach || !bind_text(attach.get(), 1, user_db_path) || sqlite3_step(attach.get()) != SQLITE_DONE)
|
||||
return false;
|
||||
attach.reset();
|
||||
if (!execute_sql(database.get(), "BEGIN IMMEDIATE"))
|
||||
return false;
|
||||
const auto rollback = [&]() {
|
||||
(void)execute_sql(database.get(), "ROLLBACK");
|
||||
return false;
|
||||
};
|
||||
// 增量语义对齐全拼侧 build_sql_for_updating_word:新权重 = 同码组当前最高权重 + 1,
|
||||
// 选中候选升到组内首位,而不是固定 +1(同码组有更高权重行时固定 +1 不会改变排序)。
|
||||
auto current = prepare(database.get(), "SELECT MAX(weight) FROM main.\"wubi86\" WHERE \"key\"=?1");
|
||||
if (!current || !bind_text(current.get(), 1, key) || sqlite3_step(current.get()) != SQLITE_ROW)
|
||||
return rollback();
|
||||
const std::int64_t new_weight = clamp_managed_weight(sqlite3_column_int64(current.get(), 0) + 1);
|
||||
current.reset();
|
||||
auto bump = prepare(database.get(), "UPDATE main.\"wubi86\" SET weight=?1 WHERE \"key\"=?2 AND \"value\"=?3");
|
||||
// changes()==0 表示目标行不存在:与 update_wubi_weight 一致,不造行。
|
||||
if (!bump || sqlite3_bind_int64(bump.get(), 1, new_weight) != SQLITE_OK || !bind_text(bump.get(), 2, key) ||
|
||||
!bind_text(bump.get(), 3, value) || sqlite3_step(bump.get()) != SQLITE_DONE ||
|
||||
sqlite3_changes(database.get()) == 0)
|
||||
return rollback();
|
||||
bump.reset();
|
||||
auto journal =
|
||||
prepare(database.get(), "INSERT INTO candidate_journal.user_dictionary_operations(dictionary,key,value,"
|
||||
"operation,weight,display) VALUES(?1,?2,?3,'upsert',?4,'')"
|
||||
" ON CONFLICT(dictionary,key,value) DO UPDATE SET operation='upsert',"
|
||||
"weight=excluded.weight,display='',updated_at=unixepoch()");
|
||||
if (!journal || !bind_text(journal.get(), 1, kind_name(DictionaryKind::Wubi)) ||
|
||||
!bind_text(journal.get(), 2, key) || !bind_text(journal.get(), 3, value) ||
|
||||
sqlite3_bind_int64(journal.get(), 4, new_weight) != SQLITE_OK || sqlite3_step(journal.get()) != SQLITE_DONE)
|
||||
return rollback();
|
||||
journal.reset();
|
||||
if (!execute_sql(database.get(), "COMMIT"))
|
||||
return rollback();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool learn_entered_english_word(const std::string &english_db_path, const std::string &user_db_path,
|
||||
const std::string &display, std::int64_t weight)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,12 @@ bool ensure_user_database(const std::string &user_db_path);
|
||||
bool record_pinyin_upsert_from_database(const std::string &main_db_path, const std::string &key,
|
||||
const std::string &value,
|
||||
const std::string &user_db_path = default_user_db_path());
|
||||
// Raise the selected wubi candidate to the top of its own code group (max weight + 1, same
|
||||
// semantics as the quanpin side) and persist the new weight plus a journal upsert in one
|
||||
// attached-database transaction. Existing rows only: a missing (key,value) pair returns false
|
||||
// without inserting. Row-missing behaviour matches update_wubi_weight.
|
||||
bool bump_wubi_weight(const std::string &main_db_path, const std::string &user_db_path, const std::string &key,
|
||||
const std::string &value);
|
||||
|
||||
struct ReplayResult
|
||||
{
|
||||
|
||||
@@ -194,7 +194,7 @@ shuangpin_preedit_mode = "shuangpin"
|
||||
# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft"
|
||||
# shuangpin_schema = "xiaohe"
|
||||
shuangpin_schema = "xiaohe"
|
||||
# 五笔方案,可选:"wubi86", "wubi98"
|
||||
# 五笔方案,当前仅支持 "wubi86";98 版待词库发布后另开任务接线
|
||||
wubi_schema = "wubi86"
|
||||
# 日语方案,当前支持:"romaji"
|
||||
japanese_schema = "romaji"
|
||||
|
||||
@@ -194,7 +194,7 @@ shuangpin_preedit_mode = "shuangpin"
|
||||
# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft"
|
||||
# shuangpin_schema = "xiaohe"
|
||||
shuangpin_schema = "xiaohe"
|
||||
# 五笔方案,可选:"wubi86", "wubi98"
|
||||
# 五笔方案,当前仅支持 "wubi86";98 版待词库发布后另开任务接线
|
||||
wubi_schema = "wubi86"
|
||||
# 日语方案,当前支持:"romaji"
|
||||
japanese_schema = "romaji"
|
||||
|
||||
+184
-16
@@ -1,10 +1,14 @@
|
||||
#include "tests/includes/test_framework.h"
|
||||
#include "tests/includes/test_utf8_path.h"
|
||||
#include "engine/contracts/assets/assets.h"
|
||||
#include "engine/core/runtime_paths.h"
|
||||
#include "engine/providers/wubi_candidate_provider.h"
|
||||
#include "engine/schemes/wubi_scheme.h"
|
||||
#include <filesystem>
|
||||
#include <sqlite3.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -17,6 +21,54 @@ void InputLetters(WubiScheme &scheme, const std::string &keys)
|
||||
}
|
||||
}
|
||||
|
||||
// 与 test_user_dictionary_journal.cpp 的同名助手同型:打开临时库跑语句、取标量。
|
||||
class TestDatabase
|
||||
{
|
||||
public:
|
||||
explicit TestDatabase(const std::filesystem::path &path)
|
||||
{
|
||||
REQUIRE_EQ(sqlite3_open(test::Utf8(path).c_str(), &db_), SQLITE_OK);
|
||||
}
|
||||
~TestDatabase()
|
||||
{
|
||||
if (db_ != nullptr)
|
||||
sqlite3_close(db_);
|
||||
}
|
||||
void exec(const std::string &sql)
|
||||
{
|
||||
REQUIRE_EQ(sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, nullptr), SQLITE_OK);
|
||||
}
|
||||
std::int64_t scalar_int64(const std::string &sql)
|
||||
{
|
||||
sqlite3_stmt *stmt = nullptr;
|
||||
REQUIRE_EQ(sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr), SQLITE_OK);
|
||||
REQUIRE_EQ(sqlite3_step(stmt), SQLITE_ROW);
|
||||
const std::int64_t value = sqlite3_column_int64(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return value;
|
||||
}
|
||||
std::int64_t scalar_int(const std::string &sql)
|
||||
{
|
||||
return scalar_int64(sql);
|
||||
}
|
||||
|
||||
private:
|
||||
sqlite3 *db_ = nullptr;
|
||||
};
|
||||
|
||||
// journal 路径经 RuntimePaths 注入临时目录,避免测试写真实用户目录;目录须先存在,
|
||||
// 与生产环境一致(RuntimePaths 管理的世代目录总是已建好)。
|
||||
metasequoia::RuntimePaths PathsIn(const std::filesystem::path &directory)
|
||||
{
|
||||
metasequoia::RuntimePaths paths;
|
||||
paths.resources = directory / "resources";
|
||||
paths.user_data = directory / "user";
|
||||
paths.cache = directory / "cache";
|
||||
paths.dictionaries = directory / "resources";
|
||||
std::filesystem::create_directories(paths.user_data);
|
||||
return paths;
|
||||
}
|
||||
|
||||
std::filesystem::path CreateWubiDatabase()
|
||||
{
|
||||
const auto path = std::filesystem::temp_directory_path() / "msime-wubi-provider-test.db";
|
||||
@@ -31,7 +83,11 @@ std::filesystem::path CreateWubiDatabase()
|
||||
const char *sql = "CREATE TABLE wubi86 (\"key\" TEXT, \"value\" TEXT, \"weight\" INTEGER);"
|
||||
"INSERT INTO wubi86 VALUES ('a', '工', 20);"
|
||||
"INSERT INTO wubi86 VALUES ('a', '戈', 10);"
|
||||
"INSERT INTO wubi86 VALUES ('aa', '式', 20);";
|
||||
"INSERT INTO wubi86 VALUES ('aa', '式', 20);"
|
||||
// 上界邻居:'y' 前缀查询必须命中 'yh',且排除上界之外的 'z' 行。
|
||||
"INSERT INTO wubi86 VALUES ('y', '也', 5);"
|
||||
"INSERT INTO wubi86 VALUES ('yh', '又', 6);"
|
||||
"INSERT INTO wubi86 VALUES ('z', '越过上界', 999);";
|
||||
const int result = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
||||
sqlite3_close(db);
|
||||
if (result != SQLITE_OK)
|
||||
@@ -41,6 +97,16 @@ std::filesystem::path CreateWubiDatabase()
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
QueryRequest WubiRequest(const std::string &code)
|
||||
{
|
||||
QueryRequest request;
|
||||
request.scheme = SchemeType::Wubi;
|
||||
request.raw_input = code;
|
||||
request.normalized_input = code;
|
||||
request.valid = true;
|
||||
return request;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(WubiSchemeBuildsDirectCodeRequest)
|
||||
@@ -66,27 +132,129 @@ TEST_CASE(WubiSchemeAcceptsAtMostFourCodesAndDoesNotUseZ)
|
||||
REQUIRE_EQ(scheme.build_request().raw_input, std::string("abc"));
|
||||
}
|
||||
|
||||
TEST_CASE(WubiProviderUsesExactCodeAndFixedWeightOrder)
|
||||
TEST_CASE(WubiProviderReturnsPrefixMatchesWithExactCodeFirst)
|
||||
{
|
||||
const auto db_path = CreateWubiDatabase();
|
||||
{
|
||||
WubiCandidateProvider provider(test::Utf8(db_path));
|
||||
QueryRequest request;
|
||||
request.scheme = SchemeType::Wubi;
|
||||
request.raw_input = "a";
|
||||
request.normalized_input = "a";
|
||||
request.valid = true;
|
||||
|
||||
const auto candidates = provider.query(request);
|
||||
REQUIRE_EQ(candidates.size(), static_cast<size_t>(2));
|
||||
REQUIRE_EQ(candidates[0].pinyin, std::string("a"));
|
||||
REQUIRE_EQ(candidates[0].word, std::string("工"));
|
||||
REQUIRE_EQ(candidates[0].weight, 20);
|
||||
REQUIRE_EQ(candidates[1].word, std::string("戈"));
|
||||
// 1 码:精确等长行在前(按权重),更长前缀行按权重跟随——逐键提示的基础。
|
||||
const auto one_code = provider.query(WubiRequest("a"));
|
||||
REQUIRE_EQ(one_code.size(), static_cast<size_t>(3));
|
||||
REQUIRE_EQ(one_code[0].pinyin, std::string("a"));
|
||||
REQUIRE_EQ(one_code[0].word, std::string("工"));
|
||||
REQUIRE_EQ(one_code[0].weight, 20);
|
||||
REQUIRE_EQ(one_code[1].word, std::string("戈"));
|
||||
REQUIRE_EQ(one_code[2].pinyin, std::string("aa"));
|
||||
REQUIRE_EQ(one_code[2].word, std::string("式"));
|
||||
|
||||
REQUIRE_EQ(provider.update_weight_by_pinyin_and_word(SchemeType::Wubi, "a", "戈"), 0);
|
||||
const auto unchanged = provider.query(request);
|
||||
REQUIRE_EQ(unchanged[0].word, std::string("工"));
|
||||
// 每一码都是前缀查询:2 码精确命中只剩等长行。
|
||||
const auto two_code = provider.query(WubiRequest("aa"));
|
||||
REQUIRE_EQ(two_code.size(), static_cast<size_t>(1));
|
||||
REQUIRE_EQ(two_code[0].word, std::string("式"));
|
||||
|
||||
// 精确优先压过权重:'y'(5) 排在前缀行 'yh'(6) 之前;上界构造排除 'z' 行。
|
||||
const auto upper_edge = provider.query(WubiRequest("y"));
|
||||
REQUIRE_EQ(upper_edge.size(), static_cast<size_t>(2));
|
||||
REQUIRE_EQ(upper_edge[0].word, std::string("也"));
|
||||
REQUIRE_EQ(upper_edge[1].word, std::string("又"));
|
||||
}
|
||||
std::filesystem::remove(db_path);
|
||||
}
|
||||
|
||||
TEST_CASE(WubiProviderLimitsPrefixScanToBoundedRows)
|
||||
{
|
||||
const auto directory =
|
||||
std::filesystem::temp_directory_path() / ("msime-wubi-provider-limit-" + std::to_string(GetCurrentProcessId()));
|
||||
std::filesystem::create_directories(directory);
|
||||
const auto db_path = directory / "limit.db";
|
||||
{
|
||||
TestDatabase db(db_path);
|
||||
std::string sql = "CREATE TABLE wubi86(\"key\" TEXT,\"value\" TEXT,\"weight\" INTEGER);";
|
||||
// 55 个精确行 + 5 个前缀行:LIMIT 截断后可见的只能是权重最高的 50 个精确行,
|
||||
// 前缀行整体排在精确组之后,不应被返回。
|
||||
for (int weight = 1; weight <= 55; ++weight)
|
||||
{
|
||||
sql += "INSERT INTO wubi86 VALUES('a','字" + std::to_string(weight) + "'," + std::to_string(weight) + ");";
|
||||
}
|
||||
for (int weight = 1; weight <= 5; ++weight)
|
||||
{
|
||||
sql += "INSERT INTO wubi86 VALUES('ab','前" + std::to_string(weight) + "'," + std::to_string(weight) + ");";
|
||||
}
|
||||
db.exec(sql);
|
||||
}
|
||||
{
|
||||
WubiCandidateProvider provider(test::Utf8(db_path));
|
||||
const auto candidates = provider.query(WubiRequest("a"));
|
||||
REQUIRE_EQ(candidates.size(), static_cast<size_t>(50));
|
||||
REQUIRE_EQ(candidates[0].weight, 55);
|
||||
REQUIRE_EQ(candidates[49].weight, 6);
|
||||
}
|
||||
std::filesystem::remove_all(directory);
|
||||
}
|
||||
|
||||
TEST_CASE(WubiProviderLearnsSelectionIntoWeightAndJournal)
|
||||
{
|
||||
const auto directory =
|
||||
std::filesystem::temp_directory_path() / ("msime-wubi-provider-learn-" + std::to_string(GetCurrentProcessId()));
|
||||
std::filesystem::create_directories(directory);
|
||||
const auto db_path = directory / "main.db";
|
||||
const auto paths = PathsIn(directory);
|
||||
{
|
||||
TestDatabase db(db_path);
|
||||
db.exec("CREATE TABLE wubi86(\"key\" TEXT,\"value\" TEXT,\"weight\" INTEGER);"
|
||||
"INSERT INTO wubi86 VALUES('aa','式',20);"
|
||||
"INSERT INTO wubi86 VALUES('aa','工',10);");
|
||||
}
|
||||
{
|
||||
WubiCandidateProvider provider(test::Utf8(db_path), paths);
|
||||
// 选中次位候选:权重升到同码组最高(MAX+1),与全拼 build_sql_for_updating_word 语义一致。
|
||||
REQUIRE_EQ(provider.update_weight_by_pinyin_and_word(SchemeType::Wubi, "aa", "工"), 0);
|
||||
const auto candidates = provider.query(WubiRequest("aa"));
|
||||
REQUIRE_EQ(candidates.size(), static_cast<size_t>(2));
|
||||
REQUIRE_EQ(candidates[0].word, std::string("工"));
|
||||
REQUIRE_EQ(candidates[0].weight, 21);
|
||||
|
||||
// 行不存在不造行。
|
||||
REQUIRE_EQ(provider.update_weight_by_pinyin_and_word(SchemeType::Wubi, "ab", "不存在"), -1);
|
||||
}
|
||||
{
|
||||
TestDatabase journal(paths.user(metasequoia::assets::user_journal));
|
||||
REQUIRE_EQ(journal.scalar_int("SELECT COUNT(*) FROM user_dictionary_operations WHERE dictionary='wubi' "
|
||||
"AND key='aa' AND value='工' AND operation='upsert' AND weight=21"),
|
||||
1);
|
||||
REQUIRE_EQ(journal.scalar_int("SELECT COUNT(*) FROM user_dictionary_operations WHERE value='不存在'"), 0);
|
||||
}
|
||||
std::filesystem::remove_all(directory);
|
||||
}
|
||||
|
||||
TEST_CASE(WubiProviderDeletesWubiCandidateThroughJournal)
|
||||
{
|
||||
const auto directory = std::filesystem::temp_directory_path() /
|
||||
("msime-wubi-provider-delete-" + std::to_string(GetCurrentProcessId()));
|
||||
std::filesystem::create_directories(directory);
|
||||
const auto db_path = directory / "main.db";
|
||||
const auto paths = PathsIn(directory);
|
||||
{
|
||||
TestDatabase db(db_path);
|
||||
db.exec("CREATE TABLE wubi86(\"key\" TEXT,\"value\" TEXT,\"weight\" INTEGER);"
|
||||
"INSERT INTO wubi86 VALUES('aa','式',20);"
|
||||
"INSERT INTO wubi86 VALUES('aa','工',10);");
|
||||
}
|
||||
{
|
||||
WubiCandidateProvider provider(test::Utf8(db_path), paths);
|
||||
REQUIRE_EQ(provider.delete_by_pinyin_and_word(SchemeType::Wubi, "aa", "式"), 0);
|
||||
// 再删已不存在的行:changes()==0,失败且不写墓碑。
|
||||
REQUIRE_EQ(provider.delete_by_pinyin_and_word(SchemeType::Wubi, "aa", "式"), -1);
|
||||
const auto remaining = provider.query(WubiRequest("aa"));
|
||||
REQUIRE_EQ(remaining.size(), static_cast<size_t>(1));
|
||||
REQUIRE_EQ(remaining[0].word, std::string("工"));
|
||||
}
|
||||
{
|
||||
TestDatabase journal(paths.user(metasequoia::assets::user_journal));
|
||||
REQUIRE_EQ(journal.scalar_int("SELECT COUNT(*) FROM user_dictionary_operations WHERE dictionary='wubi' "
|
||||
"AND key='aa' AND value='式' AND operation='delete'"),
|
||||
1);
|
||||
}
|
||||
std::filesystem::remove_all(directory);
|
||||
}
|
||||
Reference in new issue
Block a user