use samkhya_core::feedback::{FeedbackStore, PlanObservation};
use samkhya_core::residual::CorrectionFeatures;
fn temp_db(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join("samkhya_feedback_migration");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join(name);
std::fs::remove_file(&path).ok();
path
}
fn write_legacy_store(path: &std::path::Path) {
let conn = rusqlite::Connection::open(path).expect("sqlite opens");
conn.execute_batch(
"CREATE TABLE observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
template_hash TEXT NOT NULL,
plan_fingerprint TEXT NOT NULL,
est_rows INTEGER NOT NULL,
actual_rows INTEGER NOT NULL,
latency_ms REAL,
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT INTO observations (template_hash, plan_fingerprint, est_rows, actual_rows)
VALUES ('old', 'p1', 10, 100);
PRAGMA user_version = 1;",
)
.expect("legacy schema");
}
#[test]
fn an_older_store_migrates_in_place_and_keeps_its_rows() {
let db = temp_db("legacy_schema.db");
write_legacy_store(&db);
let store = FeedbackStore::open(&db).expect("opens a pre-1.2 store");
assert_eq!(store.count().expect("count"), 1);
assert_eq!(store.history("old").expect("legacy read").len(), 1);
assert!(store.plan_history("old").expect("plan read").is_empty());
store
.record_plan(&PlanObservation {
template_hash: "old".into(),
plan_fingerprint: "p2".into(),
features: CorrectionFeatures {
baseline_estimate: 20,
join_depth: 1,
..Default::default()
},
actual_rows: 200,
latency_ms: None,
})
.expect("records after migration");
assert_eq!(store.plan_history("old").expect("plan read").len(), 1);
assert_eq!(store.count().expect("count"), 2);
std::fs::remove_file(&db).ok();
}
#[test]
fn the_migration_is_idempotent() {
let db = temp_db("idempotent.db");
write_legacy_store(&db);
for _ in 0..3 {
let store = FeedbackStore::open(&db).expect("opens");
assert_eq!(store.count().expect("count"), 1);
}
let conn = rusqlite::Connection::open(&db).expect("sqlite opens");
let mut stmt = conn
.prepare("PRAGMA table_info(observations)")
.expect("pragma");
let columns: Vec<String> = stmt
.query_map([], |row| row.get::<_, String>(1))
.expect("query")
.map(|r| r.expect("row"))
.collect();
for expected in [
"left_input_rows",
"right_input_rows",
"left_distinct",
"right_distinct",
"predicate_count",
"join_depth",
] {
assert_eq!(
columns.iter().filter(|c| c.as_str() == expected).count(),
1,
"column {expected} should appear exactly once, got {columns:?}"
);
}
std::fs::remove_file(&db).ok();
}
#[test]
fn a_fresh_store_has_the_feature_columns() {
let store = FeedbackStore::open_in_memory().expect("opens");
store
.record_plan(&PlanObservation {
template_hash: "fresh".into(),
plan_fingerprint: "p".into(),
features: CorrectionFeatures {
baseline_estimate: 1,
join_depth: 2,
predicate_count: 3,
..Default::default()
},
actual_rows: 5,
latency_ms: None,
})
.expect("records");
let history = store.plan_history("fresh").expect("reads");
assert_eq!(history.len(), 1);
assert_eq!(history[0].features.predicate_count, 3);
assert_eq!(history[0].features.join_depth, 2);
}