diff --git a/crates/df-storage/src/lib.rs b/crates/df-storage/src/lib.rs index a367fb3..8a9ac82 100644 --- a/crates/df-storage/src/lib.rs +++ b/crates/df-storage/src/lib.rs @@ -3,5 +3,6 @@ pub mod crud; pub mod db; pub mod migrations; +pub mod migrations_helpers; pub mod models; pub mod secret; diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs index 119f649..d13764d 100644 --- a/crates/df-storage/src/migrations.rs +++ b/crates/df-storage/src/migrations.rs @@ -1,20 +1,9 @@ //! 数据库迁移 — 建表 SQL 与版本管理 +use crate::migrations_helpers::column_exists; use anyhow::Result; use rusqlite::Connection; -/// 检测表是否存在指定列(幂等迁移通用辅助) -fn column_exists(conn: &Connection, table: &str, col: &str) -> bool { - let Ok(mut stmt) = conn.prepare(&format!("PRAGMA table_info({table})")) else { return false }; - let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(1)) else { return false }; - for r in rows { - if let Ok(name) = r { - if name == col { return true; } - } - } - false -} - /// 执行所有迁移 pub fn run(conn: &Connection) -> Result<()> { // 创建迁移版本表 @@ -595,65 +584,3 @@ CREATE TABLE IF NOT EXISTS app_settings ( updated_at TEXT NOT NULL ); "; - -#[cfg(test)] -mod tests { - use super::*; - - // column_exists 是幂等迁移的通用探测辅助(V4+ 全部迁移靠它判断列存在性), - // 本组用 in-memory SQLite(:memory:) 建临时表覆盖四种语义: - // 列存在→true / 列不存在→false / 表不存在→false / 多列中正确匹配目标列名。 - - /// 列存在返回 true:建表后探测真实列名。 - #[test] - fn column_exists_returns_true_for_existing_column() { - let conn = Connection::open_in_memory().expect("open in-memory db"); - conn.execute_batch( - "CREATE TABLE probe_t (id TEXT PRIMARY KEY, name TEXT NOT NULL, score INTEGER);", - ) - .expect("create probe table"); - assert!(column_exists(&conn, "probe_t", "id")); - assert!(column_exists(&conn, "probe_t", "name")); - assert!(column_exists(&conn, "probe_t", "score")); - } - - /// 列不存在返回 false:表存在但目标列名未建。 - #[test] - fn column_exists_returns_false_for_missing_column() { - let conn = Connection::open_in_memory().expect("open in-memory db"); - conn.execute_batch("CREATE TABLE probe_t (id TEXT PRIMARY KEY, name TEXT);") - .expect("create probe table"); - assert!(!column_exists(&conn, "probe_t", "archived")); - assert!(!column_exists(&conn, "probe_t", "deleted_at")); - } - - /// 表不存在返回 false:PRAGMA table_info 对未知表返回空结果集,函数应安全降级为 false。 - #[test] - fn column_exists_returns_false_when_table_absent() { - let conn = Connection::open_in_memory().expect("open in-memory db"); - // 不建任何表,直接探测 - assert!(!column_exists(&conn, "ghost_table", "any_col")); - } - - /// 多列表中正确匹配目标列名(不误命中前缀/子串):验证精确比较非 contains。 - #[test] - fn column_exists_matches_exact_name_among_many() { - let conn = Connection::open_in_memory().expect("open in-memory db"); - conn.execute_batch( - "CREATE TABLE probe_t ( - id TEXT PRIMARY KEY, - prompt_tokens INTEGER, - completion_tokens INTEGER, - model TEXT - );", - ) - .expect("create probe table"); - // 命中真实列 - assert!(column_exists(&conn, "probe_t", "prompt_tokens")); - assert!(column_exists(&conn, "probe_t", "completion_tokens")); - // 不误命中子串(prefix/suffix 近似名) - assert!(!column_exists(&conn, "probe_t", "tokens")); - assert!(!column_exists(&conn, "probe_t", "prompt")); - assert!(!column_exists(&conn, "probe_t", "model_configs")); - } -} diff --git a/crates/df-storage/src/migrations_helpers.rs b/crates/df-storage/src/migrations_helpers.rs new file mode 100644 index 0000000..4c07249 --- /dev/null +++ b/crates/df-storage/src/migrations_helpers.rs @@ -0,0 +1,77 @@ +//! 数据库迁移 — 通用辅助(幂等迁移探测) + +use rusqlite::Connection; + +/// 检测表是否存在指定列(幂等迁移通用辅助) +pub(crate) fn column_exists(conn: &Connection, table: &str, col: &str) -> bool { + let Ok(mut stmt) = conn.prepare(&format!("PRAGMA table_info({table})")) else { return false }; + let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(1)) else { return false }; + for r in rows { + if let Ok(name) = r { + if name == col { return true; } + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + // column_exists 是幂等迁移的通用探测辅助(V4+ 全部迁移靠它判断列存在性), + // 本组用 in-memory SQLite(:memory:) 建临时表覆盖四种语义: + // 列存在→true / 列不存在→false / 表不存在→false / 多列中正确匹配目标列名。 + + /// 列存在返回 true:建表后探测真实列名。 + #[test] + fn column_exists_returns_true_for_existing_column() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch( + "CREATE TABLE probe_t (id TEXT PRIMARY KEY, name TEXT NOT NULL, score INTEGER);", + ) + .expect("create probe table"); + assert!(column_exists(&conn, "probe_t", "id")); + assert!(column_exists(&conn, "probe_t", "name")); + assert!(column_exists(&conn, "probe_t", "score")); + } + + /// 列不存在返回 false:表存在但目标列名未建。 + #[test] + fn column_exists_returns_false_for_missing_column() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch("CREATE TABLE probe_t (id TEXT PRIMARY KEY, name TEXT);") + .expect("create probe table"); + assert!(!column_exists(&conn, "probe_t", "archived")); + assert!(!column_exists(&conn, "probe_t", "deleted_at")); + } + + /// 表不存在返回 false:PRAGMA table_info 对未知表返回空结果集,函数应安全降级为 false。 + #[test] + fn column_exists_returns_false_when_table_absent() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + // 不建任何表,直接探测 + assert!(!column_exists(&conn, "ghost_table", "any_col")); + } + + /// 多列表中正确匹配目标列名(不误命中前缀/子串):验证精确比较非 contains。 + #[test] + fn column_exists_matches_exact_name_among_many() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch( + "CREATE TABLE probe_t ( + id TEXT PRIMARY KEY, + prompt_tokens INTEGER, + completion_tokens INTEGER, + model TEXT + );", + ) + .expect("create probe table"); + // 命中真实列 + assert!(column_exists(&conn, "probe_t", "prompt_tokens")); + assert!(column_exists(&conn, "probe_t", "completion_tokens")); + // 不误命中子串(prefix/suffix 近似名) + assert!(!column_exists(&conn, "probe_t", "tokens")); + assert!(!column_exists(&conn, "probe_t", "prompt")); + assert!(!column_exists(&conn, "probe_t", "model_configs")); + } +}