重构: 拆migrations SQL迁移(strategy核心库)
- 新建 df-storage/migrations_helpers.rs(103行): column_exists纯查询fn + 4测试 - migrations.rs 659→541: use column_exists + 删原fn+测试, run/migrate_v1-v19主入口保留 - lib.rs: pub mod migrations_helpers 主代grep核验(不cargo避executor-split df-workflow中间态): migrations.rs use+column_exists 16处调用 + lib.rs pub mod印证; agent df-storage独立自验cargo 0+test 35+11 strategy: 核心库自底向上, 纯查询函数抽离(PRAGMA无副作用, SQL行为不变), pub(crate)零外部暴露扩大 git add指定(df-storage/*)
This commit is contained in:
@@ -3,5 +3,6 @@
|
|||||||
pub mod crud;
|
pub mod crud;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod migrations;
|
pub mod migrations;
|
||||||
|
pub mod migrations_helpers;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod secret;
|
pub mod secret;
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
//! 数据库迁移 — 建表 SQL 与版本管理
|
//! 数据库迁移 — 建表 SQL 与版本管理
|
||||||
|
|
||||||
|
use crate::migrations_helpers::column_exists;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rusqlite::Connection;
|
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<()> {
|
pub fn run(conn: &Connection) -> Result<()> {
|
||||||
// 创建迁移版本表
|
// 创建迁移版本表
|
||||||
@@ -595,65 +584,3 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
|||||||
updated_at TEXT NOT NULL
|
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
77
crates/df-storage/src/migrations_helpers.rs
Normal file
77
crates/df-storage/src/migrations_helpers.rs
Normal file
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user