fix(database): close canonical restore review gaps

This commit is contained in:
SaladDay
2026-08-01 14:12:08 +00:00
parent 9a87512224
commit 89961dff28
4 changed files with 902 additions and 91 deletions
File diff suppressed because it is too large Load Diff
+28 -1
View File
@@ -100,6 +100,23 @@ pub(crate) fn validate_provider_storage_json(
if let Some(source) = meta.pricing_model_source.as_deref() { if let Some(source) = meta.pricing_model_source.as_deref() {
super::proxy::validate_pricing_source(source)?; super::proxy::validate_pricing_source(source)?;
} }
for (field, value) in [
("limitDailyUsd", meta.limit_daily_usd.as_deref()),
("limitMonthlyUsd", meta.limit_monthly_usd.as_deref()),
] {
if let Some(value) = value {
let parsed = value.parse::<rust_decimal::Decimal>().map_err(|error| {
AppError::InvalidInput(format!(
"invalid provider meta {field} for '{app_type}/{provider_id}': {error}"
))
})?;
if parsed < rust_decimal::Decimal::ZERO {
return Err(AppError::InvalidInput(format!(
"negative provider meta {field} for '{app_type}/{provider_id}'"
)));
}
}
}
Ok(()) Ok(())
} }
@@ -495,7 +512,17 @@ impl Database {
|row| row.get(0), |row| row.get(0),
) )
.map_err(|e| AppError::Database(e.to_string()))?; .map_err(|e| AppError::Database(e.to_string()))?;
Ok(max.map(|v| (v + 1) as usize).unwrap_or(0)) match max {
Some(value) => value
.checked_add(1)
.and_then(|next| usize::try_from(next).ok())
.ok_or_else(|| {
AppError::InvalidInput(format!(
"provider sort_index cannot advance past {value}"
))
}),
None => Ok(0),
}
} }
/// 启动时调用:补齐缺失的官方预设供应商(Claude / Codex / Gemini)。 /// 启动时调用:补齐缺失的官方预设供应商(Claude / Codex / Gemini)。
+22 -3
View File
@@ -25,9 +25,8 @@ impl Database {
.map_err(|e| AppError::Database(e.to_string()))?; .map_err(|e| AppError::Database(e.to_string()))?;
if let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? { if let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
Ok(Some( row.get::<_, Option<String>>(0)
row.get(0).map_err(|e| AppError::Database(e.to_string()))?, .map_err(|e| AppError::Database(e.to_string()))
))
} else { } else {
Ok(None) Ok(None)
} }
@@ -325,3 +324,23 @@ impl Database {
self.set_setting("log_config", &json) self.set_setting("log_config", &json)
} }
} }
#[cfg(test)]
mod tests {
use crate::database::{lock_conn, Database};
use crate::error::AppError;
#[test]
fn null_setting_value_hydrates_as_absent() -> Result<(), crate::error::AppError> {
let database = Database::memory()?;
{
let conn = lock_conn!(database.conn);
conn.execute(
"INSERT INTO settings (key, value) VALUES ('nullable-setting', NULL)",
[],
)?;
}
assert_eq!(database.get_setting("nullable-setting")?, None);
Ok(())
}
}
+2 -2
View File
@@ -7,7 +7,7 @@
"binaryRestoreBytes": 2147483648, "binaryRestoreBytes": 2147483648,
"scratchBytes": 2147483648 "scratchBytes": 2147483648
}, },
"specSha256": "e2b44f6fbbb1ca7287487e68aeb5684853c024978f418be688fcd15d665fd3aa", "specSha256": "f5f04260bd45bc2cd6f4766d2c66f445dad685766c1950872bc626262f113907",
"tables": [ "tables": [
{"name": "providers", "policy": "portable_incoming"}, {"name": "providers", "policy": "portable_incoming"},
{"name": "provider_endpoints", "policy": "portable_incoming"}, {"name": "provider_endpoints", "policy": "portable_incoming"},
@@ -23,7 +23,7 @@
{"name": "stream_check_logs", "policy": "portable_incoming"}, {"name": "stream_check_logs", "policy": "portable_incoming"},
{"name": "proxy_live_backup", "policy": "portable_incoming"}, {"name": "proxy_live_backup", "policy": "portable_incoming"},
{"name": "usage_daily_rollups", "policy": "portable_incoming"}, {"name": "usage_daily_rollups", "policy": "portable_incoming"},
{"name": "session_log_sync", "policy": "portable_incoming"}, {"name": "session_log_sync", "policy": "preserve_live"},
{"name": "profiles", "policy": "portable_incoming"}, {"name": "profiles", "policy": "portable_incoming"},
{"name": "pi_provider_projections", "policy": "preserve_live"}, {"name": "pi_provider_projections", "policy": "preserve_live"},
{"name": "skill_deployments", "policy": "preserve_live"} {"name": "skill_deployments", "policy": "preserve_live"}