Merge branch 'main' of https://git.cancerai.top/qing/blackbird
This commit is contained in:
commit
cc0475e5dc
2 files changed
+234
-38
No files matched your search
+81
-10
@@ -47,6 +47,10 @@ using namespace pmc::net;
|
||||
static constexpr size_t kMaxMessage = 8192;
|
||||
static constexpr size_t kMaxExtra = 4096;
|
||||
|
||||
// 输入规模限制(防资源耗尽)
|
||||
static constexpr size_t kMaxWhereConditions = 64; // 单请求 WHERE 条件数上限
|
||||
static constexpr size_t kMaxInElements = 1000; // 单个 IN 运算符元素上限
|
||||
|
||||
// ============================================================================
|
||||
// 工具:当前 UTC 时间 ISO 8601
|
||||
// ============================================================================
|
||||
@@ -151,6 +155,25 @@ static Config load_config(int argc, char* argv[]) {
|
||||
|
||||
// 3. 解析 INI(白名单字段)
|
||||
IniMap ini = parse_ini(ini_text);
|
||||
|
||||
// 校验必需配置项(confs 提供的 db.ini 必须包含完整配置)
|
||||
static const std::pair<const char*, const char*> required_keys[] = {
|
||||
{"server", "host"},
|
||||
{"server", "port"},
|
||||
{"server", "workers"},
|
||||
{"database", "url"},
|
||||
{"logs", "retention_days"},
|
||||
{"logs", "max_records"},
|
||||
{"logs", "cleanup_interval"},
|
||||
};
|
||||
for (auto& [sec, key] : required_keys) {
|
||||
auto si = ini.find(sec);
|
||||
if (si == ini.end() || si->second.find(key) == si->second.end()) {
|
||||
std::fprintf(stderr, "[db2] missing required config: [%s] %s\n", sec, key);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
auto get = [&](const std::string& sec, const std::string& key,
|
||||
const std::string& def = "") -> std::string {
|
||||
auto si = ini.find(sec);
|
||||
@@ -243,6 +266,12 @@ struct ColInfo {
|
||||
std::string type; // 原始类型字符串,大写,如 "DATETIME" "TEXT" "INTEGER"
|
||||
};
|
||||
|
||||
// 表 schema 缓存项,携带 PRAGMA schema_version 以感知 DDL 变更
|
||||
struct SchemaCacheEntry {
|
||||
int schema_version;
|
||||
std::vector<ColInfo> cols;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// DB2Server
|
||||
// ============================================================================
|
||||
@@ -295,7 +324,7 @@ private:
|
||||
|
||||
// 表列信息缓存
|
||||
std::mutex _schema_mtx;
|
||||
std::unordered_map<std::string, std::vector<ColInfo>> _schema_cache;
|
||||
std::unordered_map<std::string, SchemaCacheEntry> _schema_cache;
|
||||
|
||||
// 后台清理线程
|
||||
std::atomic<bool> _cleanup_running{false};
|
||||
@@ -313,15 +342,32 @@ private:
|
||||
|
||||
// ---- 列 schema 反射 --------------------------------------------------------
|
||||
|
||||
static int _get_schema_version(sqlite3* db) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db, "PRAGMA schema_version", -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
throw std::runtime_error("PRAGMA schema_version failed");
|
||||
}
|
||||
int version = -1;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
version = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return version;
|
||||
}
|
||||
|
||||
std::vector<ColInfo> _get_schema(const std::string& table) {
|
||||
DbConn& conn = _get_conn();
|
||||
const int current_version = _get_schema_version(conn.get());
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(_schema_mtx);
|
||||
auto it = _schema_cache.find(table);
|
||||
if (it != _schema_cache.end()) return it->second;
|
||||
if (it != _schema_cache.end() && it->second.schema_version == current_version) {
|
||||
return it->second.cols;
|
||||
}
|
||||
}
|
||||
|
||||
// PRAGMA table_info 返回: cid, name, type, notnull, dflt_value, pk
|
||||
DbConn& conn = _get_conn();
|
||||
std::string sql = "PRAGMA table_info(" + table + ")";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
@@ -346,8 +392,8 @@ private:
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(_schema_mtx);
|
||||
_schema_cache[table] = cols;
|
||||
return cols;
|
||||
_schema_cache[table] = SchemaCacheEntry{current_version, std::move(cols)};
|
||||
return _schema_cache[table].cols;
|
||||
}
|
||||
|
||||
// 判断列是否为日期时间类型
|
||||
@@ -414,6 +460,12 @@ private:
|
||||
WhereClause wc;
|
||||
std::vector<std::string> parts;
|
||||
|
||||
if (where.size() > kMaxWhereConditions) {
|
||||
throw std::runtime_error("too many where conditions: "
|
||||
+ std::to_string(where.size())
|
||||
+ " (max " + std::to_string(kMaxWhereConditions) + ")");
|
||||
}
|
||||
|
||||
// 建立列名 -> ColInfo 映射
|
||||
std::unordered_map<std::string, ColInfo> col_map;
|
||||
for (auto& c : cols) col_map[c.name] = c;
|
||||
@@ -452,6 +504,11 @@ private:
|
||||
parts.push_back("0");
|
||||
continue;
|
||||
}
|
||||
if (op_val.as_array().size() > kMaxInElements) {
|
||||
throw std::runtime_error("too many elements in 'in' operator: "
|
||||
+ std::to_string(op_val.as_array().size())
|
||||
+ " (max " + std::to_string(kMaxInElements) + ")");
|
||||
}
|
||||
std::string placeholders;
|
||||
for (auto& item : op_val.as_array()) {
|
||||
if (!placeholders.empty()) placeholders += ", ";
|
||||
@@ -459,6 +516,8 @@ private:
|
||||
wc.params.push_back(_coerce_value(ci, item));
|
||||
}
|
||||
parts.push_back(col + " IN (" + placeholders + ")");
|
||||
} else {
|
||||
throw std::runtime_error("unknown operator: " + op);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -559,10 +618,18 @@ private:
|
||||
}
|
||||
|
||||
// limit / offset
|
||||
if (auto it = req.find("limit"); it != req.end() && it->value().is_int64())
|
||||
sql += " LIMIT " + std::to_string(it->value().as_int64());
|
||||
if (auto it = req.find("offset"); it != req.end() && it->value().is_int64())
|
||||
sql += " OFFSET " + std::to_string(it->value().as_int64());
|
||||
if (auto it = req.find("limit"); it != req.end() && it->value().is_int64()) {
|
||||
std::int64_t limit = it->value().as_int64();
|
||||
if (limit < 0)
|
||||
throw std::runtime_error("limit must be non-negative");
|
||||
sql += " LIMIT " + std::to_string(limit);
|
||||
}
|
||||
if (auto it = req.find("offset"); it != req.end() && it->value().is_int64()) {
|
||||
std::int64_t offset = it->value().as_int64();
|
||||
if (offset < 0)
|
||||
throw std::runtime_error("offset must be non-negative");
|
||||
sql += " OFFSET " + std::to_string(offset);
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
@@ -611,6 +678,8 @@ private:
|
||||
|
||||
DbConn& conn = _get_conn();
|
||||
const json::object& vals = req.at("values").as_object();
|
||||
if (vals.empty())
|
||||
throw std::runtime_error("insert requires non-empty 'values'");
|
||||
|
||||
// 建列名 -> ColInfo 映射
|
||||
std::unordered_map<std::string, ColInfo> col_map;
|
||||
@@ -653,6 +722,8 @@ private:
|
||||
|
||||
DbConn& conn = _get_conn();
|
||||
const json::object& vals = req.at("values").as_object();
|
||||
if (vals.empty())
|
||||
throw std::runtime_error("update requires non-empty 'values'");
|
||||
|
||||
std::unordered_map<std::string, ColInfo> col_map;
|
||||
for (auto& c : cols) col_map[c.name] = c;
|
||||
@@ -790,7 +861,7 @@ private:
|
||||
|
||||
// 1. 删除超过 retention_days 的记录
|
||||
std::string cutoff_sql =
|
||||
"DELETE FROM Logs WHERE ts < datetime('now', '-"
|
||||
"DELETE FROM Logs WHERE ts < strftime('%Y-%m-%dT%H:%M:%S','now', '-"
|
||||
+ std::to_string(_cfg.retention_days) + " days')";
|
||||
conn.exec(cutoff_sql);
|
||||
int expired = sqlite3_changes(conn.get());
|
||||
|
||||
Reference in new issue
Block a user