这四种模式每个键都新开一次 SQLite 连接,第一次 prepare 还要解析整个 schema ——msime.db 有两百多张分表。新增 local_modes/local_database,四个查询统一经它 打开只读连接:路径正是当前数据目录下的 msime.db 或 others.db 时共享一条连接, 其余路径(测试、工具)照旧每次独立打开、用完即关。 只共享这两个随安装包发布的库:msime.db 本来就被引擎整个会话持有,others.db 只在 Server 停止时由安装程序替换;保持其它文件常开会让删除它们的工具或测试在 Windows 上失败。close_default_user_database() 同时释放这些连接,调用方原本就用它在删除 或替换数据目录前放开文件。 引擎测试(本地单独构建,CI 不跑)23/23 通过;不加释放钩子时 input_session、 mixed_expressive_input_session、jianpin_input_session 会因目录删不掉而失败。
221 lines
6.5 KiB
C++
221 lines
6.5 KiB
C++
#include "../contracts/assets/assets.h"
|
|
#include "jianpin_query.h"
|
|
#include "local_database.h"
|
|
|
|
#include "../core/data_path.h"
|
|
#include "../quanpin/quanpin_query.h"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace metasequoia::local_modes
|
|
{
|
|
namespace
|
|
{
|
|
struct StatementCloser
|
|
{
|
|
void operator()(sqlite3_stmt *statement) const
|
|
{
|
|
if (statement != nullptr)
|
|
{
|
|
sqlite3_finalize(statement);
|
|
}
|
|
}
|
|
};
|
|
|
|
using Statement = std::unique_ptr<sqlite3_stmt, StatementCloser>;
|
|
|
|
std::string normalize_code(const std::string &code)
|
|
{
|
|
std::string normalized;
|
|
normalized.reserve(code.size());
|
|
for (unsigned char character : code)
|
|
{
|
|
if (character >= 'A' && character <= 'Z')
|
|
{
|
|
character = static_cast<unsigned char>(character - 'A' + 'a');
|
|
}
|
|
if (character < 'a' || character > 'z')
|
|
{
|
|
return {};
|
|
}
|
|
normalized.push_back(static_cast<char>(character));
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
std::string decode_shuangpin_initial(char code, const ShuangpinProfile &profile)
|
|
{
|
|
const std::string mapped_key(1, code);
|
|
for (const auto &[initial, key] : profile.initials)
|
|
{
|
|
if (key == mapped_key)
|
|
{
|
|
return initial;
|
|
}
|
|
}
|
|
return mapped_key;
|
|
}
|
|
|
|
quanpin::Segments expand_code(const std::string &normalized, SchemeType scheme, const ShuangpinProfile &profile)
|
|
{
|
|
quanpin::Segments segments;
|
|
segments.reserve(normalized.size());
|
|
for (const char character : normalized)
|
|
{
|
|
segments.push_back(scheme == SchemeType::Shuangpin ? decode_shuangpin_initial(character, profile)
|
|
: std::string(1, character));
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
std::vector<std::string> split_key(const std::string &key)
|
|
{
|
|
std::vector<std::string> parts;
|
|
std::size_t start = 0;
|
|
while (true)
|
|
{
|
|
const std::size_t separator = key.find('\'', start);
|
|
if (separator == std::string::npos)
|
|
{
|
|
parts.push_back(key.substr(start));
|
|
return parts;
|
|
}
|
|
parts.push_back(key.substr(start, separator - start));
|
|
start = separator + 1;
|
|
}
|
|
}
|
|
|
|
std::string syllable_initial(const std::string &syllable)
|
|
{
|
|
if (syllable.size() >= 2)
|
|
{
|
|
const std::string prefix = syllable.substr(0, 2);
|
|
if (prefix == "zh" || prefix == "ch" || prefix == "sh")
|
|
{
|
|
return prefix;
|
|
}
|
|
}
|
|
return syllable.empty() ? std::string{} : syllable.substr(0, 1);
|
|
}
|
|
|
|
bool key_matches_initials(const std::string &key, const quanpin::Segments &initials)
|
|
{
|
|
const auto syllables = split_key(key);
|
|
if (syllables.size() != initials.size())
|
|
{
|
|
return false;
|
|
}
|
|
for (std::size_t index = 0; index < initials.size(); ++index)
|
|
{
|
|
if (syllable_initial(syllables[index]) != initials[index])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int scan_limit(int limit, bool filter_initials)
|
|
{
|
|
if (!filter_initials || limit > 100000000)
|
|
{
|
|
return limit;
|
|
}
|
|
return std::max(limit * 32, 512);
|
|
}
|
|
|
|
LocalQueryResult query_failure(const char *diagnostic)
|
|
{
|
|
return {{}, std::string(diagnostic)};
|
|
}
|
|
} // namespace
|
|
|
|
std::string jianpin_ranking_context(const std::string &code, SchemeType scheme, const ShuangpinProfile &profile)
|
|
{
|
|
const std::string normalized = normalize_code(code);
|
|
return normalized.empty() ? std::string{} : quanpin::join_segments(expand_code(normalized, scheme, profile));
|
|
}
|
|
|
|
LocalQueryResult query_jianpin(const std::string &code, SchemeType scheme, int limit, const ShuangpinProfile &profile)
|
|
{
|
|
return query_jianpin(code, scheme, data_file_path(metasequoia::assets::main_dictionary), limit, profile);
|
|
}
|
|
|
|
LocalQueryResult query_jianpin(const std::string &code, SchemeType scheme, const std::filesystem::path &database_path,
|
|
int limit, const ShuangpinProfile &profile)
|
|
{
|
|
if (limit <= 0)
|
|
{
|
|
return {};
|
|
}
|
|
const std::string normalized = normalize_code(code);
|
|
if (normalized.empty())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
const auto segments = expand_code(normalized, scheme, profile);
|
|
const std::string table = quanpin::build_table_name(segments);
|
|
if (table.empty())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
const std::shared_ptr<sqlite3> database = open_local_database(database_path);
|
|
if (!database)
|
|
{
|
|
return query_failure("Super-jianpin database is unavailable.");
|
|
}
|
|
|
|
const std::string jianpin = quanpin::segments_to_jianpin(segments);
|
|
const bool filter_initials = scheme == SchemeType::Shuangpin;
|
|
const std::string sql =
|
|
"SELECT \"key\",\"value\",\"weight\" FROM \"" + table + "\" WHERE \"jp\"=?1 ORDER BY \"weight\" DESC LIMIT ?2";
|
|
sqlite3_stmt *raw_statement = nullptr;
|
|
if (sqlite3_prepare_v2(database.get(), sql.c_str(), -1, &raw_statement, nullptr) != SQLITE_OK)
|
|
{
|
|
return query_failure("Super-jianpin database could not be queried.");
|
|
}
|
|
Statement statement(raw_statement);
|
|
if (sqlite3_bind_text(statement.get(), 1, jianpin.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_int(statement.get(), 2, scan_limit(limit, filter_initials)) != SQLITE_OK)
|
|
{
|
|
return query_failure("Super-jianpin database could not be queried.");
|
|
}
|
|
|
|
const std::string matched_code = quanpin::join_segments(segments);
|
|
LocalQueryResult result;
|
|
int step_result = SQLITE_ROW;
|
|
while ((step_result = sqlite3_step(statement.get())) == SQLITE_ROW)
|
|
{
|
|
const auto *key = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 0));
|
|
const auto *value = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 1));
|
|
if (value == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string canonical = key == nullptr ? std::string{} : std::string(key);
|
|
if (filter_initials && !key_matches_initials(canonical, segments))
|
|
{
|
|
continue;
|
|
}
|
|
result.candidates.emplace_back(matched_code, value, sqlite3_column_int64(statement.get(), 2),
|
|
CandidateSource::Database, canonical);
|
|
if (static_cast<int>(result.candidates.size()) >= limit)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (step_result != SQLITE_DONE && step_result != SQLITE_ROW)
|
|
{
|
|
return query_failure("Super-jianpin database could not be queried.");
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace metasequoia::local_modes
|