mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 03:32:25 +08:00
refactor(database): construct untrusted migrations from source specs
This commit is contained in:
@@ -1359,7 +1359,13 @@ impl UntrustedScratch {
|
||||
self.connection
|
||||
.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)
|
||||
.map_err(|error| AppError::Database(error.to_string()))?;
|
||||
Database::create_tables_on_conn(&self.connection, MigrationRunContext::UntrustedRestore)?;
|
||||
self.connection
|
||||
.execute_batch("PRAGMA foreign_keys = OFF;")
|
||||
.map_err(|error| AppError::Database(error.to_string()))?;
|
||||
// Source recognition is deliberately first. In particular, never
|
||||
// create current tables in this connection: doing so would turn a
|
||||
// missing source table into an apparently valid empty table.
|
||||
Database::validate_untrusted_migration_source(&self.connection)?;
|
||||
Database::apply_schema_migrations_on_conn(
|
||||
&self.connection,
|
||||
MigrationRunContext::UntrustedRestore,
|
||||
@@ -1612,7 +1618,12 @@ fn validate_restore_row(spec: &RestoreTableSpec, values: &[Value]) -> Result<(),
|
||||
let meta = text_value(spec.name, "meta", &values[11])?;
|
||||
crate::database::dao::providers::validate_provider_storage_json(
|
||||
app_type, id, settings, meta,
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| {
|
||||
AppError::InvalidInput(format!(
|
||||
"restore provider row '{app_type}/{id}' is not decodable: {error}"
|
||||
))
|
||||
})?;
|
||||
crate::database::validate_cost_multiplier(text_value(
|
||||
spec.name,
|
||||
"cost_multiplier",
|
||||
@@ -2511,6 +2522,11 @@ impl Database {
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn dump_sql_for_migration_test(conn: &Connection) -> Result<String, AppError> {
|
||||
Self::dump_sql(conn, &[])
|
||||
}
|
||||
|
||||
/// 获取表的列名列表
|
||||
fn get_table_columns(conn: &Connection, table: &str) -> Result<Vec<String>, AppError> {
|
||||
let mut stmt = conn
|
||||
@@ -2904,6 +2920,94 @@ mod tests {
|
||||
source.snapshot_to_memory()
|
||||
}
|
||||
|
||||
fn exact_version_source_with_pi_provider(version: i32) -> Result<Connection, AppError> {
|
||||
let source = crate::database::migration_source::exact_migration_source_for_test(version)?;
|
||||
let provider_id = format!("migration-v{version}");
|
||||
let settings_config =
|
||||
crate::database::migration_source::pinned_pi_provider_settings_for_test(version);
|
||||
if version == 1 {
|
||||
source.execute(
|
||||
"INSERT INTO providers (
|
||||
id, app_type, name, settings_config, website_url, category,
|
||||
created_at, sort_index, notes, icon, icon_color, meta, is_current
|
||||
) VALUES (
|
||||
?1, 'pi', ?2, ?3, ?4, 'custom', 1700000001, 1,
|
||||
'Pi migration sentinel', 'pi', '#13579b', '{}', 1
|
||||
)",
|
||||
rusqlite::params![
|
||||
provider_id,
|
||||
format!("Migration v{version}"),
|
||||
settings_config,
|
||||
format!("https://pi-v{version}.example")
|
||||
],
|
||||
)?;
|
||||
source.execute_batch(
|
||||
"INSERT INTO proxy_config (
|
||||
id, proxy_enabled, listen_address, listen_port, enable_logging,
|
||||
max_retries, streaming_first_byte_timeout,
|
||||
streaming_idle_timeout, non_streaming_timeout
|
||||
) VALUES (1, 1, '10.20.30.40', 23456, 0, 9, 61, 122, 603);
|
||||
INSERT INTO circuit_breaker_config (
|
||||
id, failure_threshold, success_threshold, timeout_seconds,
|
||||
error_rate_threshold, min_requests
|
||||
) VALUES (1, 7, 4, 73, 0.375, 21);
|
||||
INSERT INTO settings (key, value) VALUES
|
||||
('proxy_takeover_claude', 'true'),
|
||||
('auto_failover_enabled_claude', 'false'),
|
||||
('proxy_takeover_codex', 'false'),
|
||||
('auto_failover_enabled_codex', 'true'),
|
||||
('proxy_takeover_gemini', 'true'),
|
||||
('auto_failover_enabled_gemini', 'true');",
|
||||
)?;
|
||||
} else {
|
||||
source.execute(
|
||||
"INSERT INTO providers (
|
||||
id, app_type, name, settings_config, website_url, category,
|
||||
created_at, sort_index, notes, icon, icon_color, meta,
|
||||
is_current, in_failover_queue, cost_multiplier,
|
||||
limit_daily_usd, limit_monthly_usd, provider_type
|
||||
) VALUES (
|
||||
?1, 'pi', ?2, ?3, ?4, 'custom', 1700000001, 1,
|
||||
'Pi migration sentinel', 'pi', '#13579b', '{}',
|
||||
1, 0, '1.25', '42.50', '420.50', 'pi-native'
|
||||
)",
|
||||
rusqlite::params![
|
||||
provider_id,
|
||||
format!("Migration v{version}"),
|
||||
settings_config,
|
||||
format!("https://pi-v{version}.example")
|
||||
],
|
||||
)?;
|
||||
}
|
||||
if version == SCHEMA_VERSION {
|
||||
source.execute(
|
||||
"INSERT INTO provider_endpoints (
|
||||
id, provider_id, app_type, url, added_at, last_used
|
||||
) VALUES (?1, ?2, 'pi', ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
10_000 + version,
|
||||
provider_id,
|
||||
format!("https://pi-endpoint-v{version}.example/v1"),
|
||||
1_700_000_000 + version,
|
||||
1_700_100_000 + version
|
||||
],
|
||||
)?;
|
||||
} else {
|
||||
source.execute(
|
||||
"INSERT INTO provider_endpoints (
|
||||
id, provider_id, app_type, url, added_at
|
||||
) VALUES (?1, ?2, 'pi', ?3, ?4)",
|
||||
rusqlite::params![
|
||||
10_000 + version,
|
||||
provider_id,
|
||||
format!("https://pi-endpoint-v{version}.example/v1"),
|
||||
1_700_000_000 + version
|
||||
],
|
||||
)?;
|
||||
}
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn actual_v16_duplicate_endpoint_source() -> Result<Connection, AppError> {
|
||||
let source = canonical_restore_source()?;
|
||||
source.execute_batch(
|
||||
@@ -3904,9 +4008,9 @@ mod tests {
|
||||
})?;
|
||||
let _home_guard = TestHomeGuard::set(test_home.path());
|
||||
|
||||
// Exercise the oldest supported layout, rather than merely relabeling
|
||||
// a current schema. Missing columns must be supplied by the real
|
||||
// v0..current migration chain before fixed-column restore begins.
|
||||
// The source-spec authority supports exactly v1..v17. A legacy v0
|
||||
// label must fail before migration DDL instead of being interpreted
|
||||
// through the permissive local-upgrade path.
|
||||
for (entry_index, entry_point) in [RestoreEntryPoint::Sql, RestoreEntryPoint::Binary]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -3929,22 +4033,20 @@ mod tests {
|
||||
PRAGMA user_version = 0;",
|
||||
)?;
|
||||
let target = Database::memory()?;
|
||||
run_restore_entry(
|
||||
let error = run_restore_entry(
|
||||
&target,
|
||||
&oldest,
|
||||
entry_point,
|
||||
&format!("actual-v0-{entry_index}.db"),
|
||||
)?;
|
||||
let conn = crate::database::lock_conn!(target.conn);
|
||||
let restored: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM providers
|
||||
WHERE id = 'actual-v0-provider'
|
||||
AND app_type = 'pi'
|
||||
AND cost_multiplier = '1.0'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
assert_eq!(restored, 1, "actual oldest-layout migration sentinel");
|
||||
)
|
||||
.expect_err("untrusted v0 is outside the declared source-spec range");
|
||||
assert!(
|
||||
matches!(error, AppError::InvalidInput(_))
|
||||
&& error
|
||||
.to_string()
|
||||
.contains("unsupported restore user_version 0"),
|
||||
"v0 must fail at source recognition via entry {entry_index}: {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Exercise the real immediately-previous v16 shape. In particular,
|
||||
@@ -3993,69 +4095,55 @@ mod tests {
|
||||
assert_eq!(sentinel, (None, 0, 0, 0));
|
||||
}
|
||||
|
||||
// Every version still gets a public dispatch sentinel. The separate
|
||||
// historical-layout cases above prevent this matrix from passing only
|
||||
// because current tables were stamped with an older user_version.
|
||||
for version in 0..=SCHEMA_VERSION {
|
||||
let source_db = Database::memory()?;
|
||||
// Every supported version is materialized from its exact source spec.
|
||||
// Each public entry must preserve a real Pi provider and its endpoint;
|
||||
// this cannot pass by stamping a current database with an old version.
|
||||
for version in 1..=SCHEMA_VERSION {
|
||||
for (entry_index, entry_point) in [RestoreEntryPoint::Sql, RestoreEntryPoint::Binary]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let conn = crate::database::lock_conn!(source_db.conn);
|
||||
conn.execute(
|
||||
"INSERT INTO providers (id, app_type, name, settings_config, meta)
|
||||
VALUES (?1, 'pi', ?2, '{}', '{}')",
|
||||
rusqlite::params![
|
||||
format!("migration-v{version}"),
|
||||
format!("Migration v{version}")
|
||||
],
|
||||
let source = exact_version_source_with_pi_provider(version)?;
|
||||
let target = Database::memory()?;
|
||||
run_restore_entry(
|
||||
&target,
|
||||
&source,
|
||||
entry_point,
|
||||
&format!("exact-migration-v{version}-{entry_index}.db"),
|
||||
)?;
|
||||
Database::set_user_version(&conn, version)?;
|
||||
}
|
||||
let source = source_db.snapshot_to_memory()?;
|
||||
let target = Database::memory()?;
|
||||
run_restore_entry(
|
||||
&target,
|
||||
&source,
|
||||
RestoreEntryPoint::Sql,
|
||||
&format!("unused-migration-v{version}.db"),
|
||||
)?;
|
||||
let conn = crate::database::lock_conn!(target.conn);
|
||||
let restored: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM providers WHERE id = ?1 AND app_type = 'pi'",
|
||||
[format!("migration-v{version}")],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
assert_eq!(restored, 1, "SQL migration sentinel v{version}");
|
||||
}
|
||||
|
||||
for version in [0, SCHEMA_VERSION - 1, SCHEMA_VERSION] {
|
||||
let source_db = Database::memory()?;
|
||||
{
|
||||
let conn = crate::database::lock_conn!(source_db.conn);
|
||||
conn.execute(
|
||||
"INSERT INTO providers (id, app_type, name, settings_config, meta)
|
||||
VALUES (?1, 'pi', ?2, '{}', '{}')",
|
||||
rusqlite::params![
|
||||
format!("binary-migration-v{version}"),
|
||||
format!("Binary migration v{version}")
|
||||
],
|
||||
let conn = crate::database::lock_conn!(target.conn);
|
||||
let restored: (String, String, String, Option<i64>) = conn.query_row(
|
||||
"SELECT p.settings_config, p.cost_multiplier, e.url, e.last_used
|
||||
FROM providers AS p
|
||||
JOIN provider_endpoints AS e
|
||||
ON e.provider_id = p.id AND e.app_type = p.app_type
|
||||
WHERE p.id = ?1 AND p.app_type = 'pi'",
|
||||
[format!("migration-v{version}")],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)?;
|
||||
Database::set_user_version(&conn, version)?;
|
||||
assert!(
|
||||
restored.0.contains("\"id\":\"fractional\"")
|
||||
&& restored
|
||||
.0
|
||||
.contains(&format!("\"migrationVersion\":{version}")),
|
||||
"Pi settings payload was not preserved for v{version}"
|
||||
);
|
||||
assert_eq!(
|
||||
restored.1,
|
||||
if version == 1 { "1.0" } else { "1.25" },
|
||||
"provider migration sentinel v{version}"
|
||||
);
|
||||
assert_eq!(
|
||||
restored.2,
|
||||
format!("https://pi-endpoint-v{version}.example/v1"),
|
||||
"endpoint migration sentinel v{version}"
|
||||
);
|
||||
assert_eq!(
|
||||
restored.3,
|
||||
(version == SCHEMA_VERSION).then_some(1_700_100_000 + i64::from(version)),
|
||||
"endpoint last_used migration sentinel v{version}"
|
||||
);
|
||||
}
|
||||
let source = source_db.snapshot_to_memory()?;
|
||||
let target = Database::memory()?;
|
||||
run_restore_entry(
|
||||
&target,
|
||||
&source,
|
||||
RestoreEntryPoint::Binary,
|
||||
&format!("migration-v{version}.db"),
|
||||
)?;
|
||||
let conn = crate::database::lock_conn!(target.conn);
|
||||
let restored: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM providers WHERE id = ?1 AND app_type = 'pi'",
|
||||
[format!("binary-migration-v{version}")],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
assert_eq!(restored, 1, "binary migration sentinel v{version}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ mod backup_restore_certification;
|
||||
mod backup_restore_certification_ext;
|
||||
mod dao;
|
||||
mod migration;
|
||||
mod migration_source;
|
||||
mod schema;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+683
-480
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -21,7 +21,11 @@
|
||||
["icon_color", "TEXT", false, null, 0],
|
||||
["meta", "TEXT", true, "'{}'", 0],
|
||||
["is_current", "BOOLEAN", true, "0", 0],
|
||||
["in_failover_queue", "BOOLEAN", true, "0", 0]
|
||||
["in_failover_queue", "BOOLEAN", true, "0", 0],
|
||||
["cost_multiplier", "TEXT", true, "'1.0'", 0],
|
||||
["limit_daily_usd", "TEXT", false, null, 0],
|
||||
["limit_monthly_usd", "TEXT", false, null, 0],
|
||||
["provider_type", "TEXT", false, null, 0]
|
||||
],
|
||||
"uniqueTuples": [],
|
||||
"foreignKeys": [],
|
||||
|
||||
Reference in New Issue
Block a user