use std::time::{SystemTime, UNIX_EPOCH};
use super::sqlite_adapter::{SqliteDatabase, SqliteError, SqliteParams, SqliteResult, SqliteValue};
pub const CURRENT_SCHEMA_VERSION: i64 = 5;
#[derive(Clone, Copy)]
pub struct Migration {
pub version: i64,
pub description: &'static str,
pub up: fn(&mut dyn SqliteDatabase) -> SqliteResult<()>,
}
fn migration_v2(db: &mut dyn SqliteDatabase) -> SqliteResult<()> {
db.exec(
r#"
CREATE TABLE IF NOT EXISTS project_metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
ALTER TABLE unresolved_refs ADD COLUMN file_path TEXT NOT NULL DEFAULT '';
ALTER TABLE unresolved_refs ADD COLUMN language TEXT NOT NULL DEFAULT 'unknown';
ALTER TABLE edges ADD COLUMN provenance TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_unresolved_file_path ON unresolved_refs(file_path);
CREATE INDEX IF NOT EXISTS idx_edges_provenance ON edges(provenance);
"#,
)
}
fn migration_v3(db: &mut dyn SqliteDatabase) -> SqliteResult<()> {
db.exec(
r#"
CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
"#,
)
}
fn migration_v4(db: &mut dyn SqliteDatabase) -> SqliteResult<()> {
db.exec(
r#"
DROP INDEX IF EXISTS idx_edges_source;
DROP INDEX IF EXISTS idx_edges_target;
"#,
)
}
fn migration_v5(db: &mut dyn SqliteDatabase) -> SqliteResult<()> {
db.exec(
r#"
ALTER TABLE nodes ADD COLUMN return_type TEXT;
"#,
)
}
pub fn migrations() -> Vec<Migration> {
vec![
Migration {
version: 2,
description: "Add project metadata, provenance tracking, and unresolved ref context",
up: migration_v2,
},
Migration {
version: 3,
description: "Add lower(name) expression index for memory-efficient case-insensitive lookups",
up: migration_v3,
},
Migration {
version: 4,
description: "Drop redundant idx_edges_source / idx_edges_target (covered by source_kind / target_kind composites)",
up: migration_v4,
},
Migration {
version: 5,
description: "Add nodes.return_type - normalized return/result type for receiver-type inference (C++ singletons/factories, #645)",
up: migration_v5,
},
]
}
pub fn get_current_version(db: &mut dyn SqliteDatabase) -> SqliteResult<i64> {
let mut stmt = match db.prepare("SELECT MAX(version) as version FROM schema_versions") {
Ok(stmt) => stmt,
Err(_) => return Ok(0),
};
let row = match stmt.get(SqliteParams::none()) {
Ok(row) => row,
Err(_) => return Ok(0),
};
Ok(row
.and_then(|mut row| row.remove("version"))
.and_then(|value| value.as_i64())
.unwrap_or(0))
}
fn record_migration(
db: &mut dyn SqliteDatabase,
version: i64,
description: &str,
) -> SqliteResult<()> {
let mut stmt = db.prepare(
"INSERT INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)",
)?;
stmt.run(SqliteParams::positional(vec![
SqliteValue::from(version),
SqliteValue::from(current_time_millis()),
SqliteValue::from(description),
]))?;
Ok(())
}
pub fn run_migrations(db: &mut dyn SqliteDatabase, from_version: i64) -> SqliteResult<()> {
let mut pending = migrations()
.into_iter()
.filter(|migration| migration.version > from_version)
.collect::<Vec<_>>();
if pending.is_empty() {
return Ok(());
}
pending.sort_by_key(|migration| migration.version);
for migration in pending {
db.transaction(&mut |tx| {
(migration.up)(tx)?;
record_migration(tx, migration.version, migration.description)
})?;
}
Ok(())
}
pub fn needs_migration(db: &mut dyn SqliteDatabase) -> SqliteResult<bool> {
Ok(get_current_version(db)? < CURRENT_SCHEMA_VERSION)
}
pub fn get_pending_migrations(db: &mut dyn SqliteDatabase) -> SqliteResult<Vec<Migration>> {
let current = get_current_version(db)?;
let mut pending = migrations()
.into_iter()
.filter(|migration| migration.version > current)
.collect::<Vec<_>>();
pending.sort_by_key(|migration| migration.version);
Ok(pending)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationHistoryEntry {
pub version: i64,
pub applied_at: i64,
pub description: Option<String>,
}
pub fn get_migration_history(
db: &mut dyn SqliteDatabase,
) -> SqliteResult<Vec<MigrationHistoryEntry>> {
let mut stmt = db
.prepare("SELECT version, applied_at, description FROM schema_versions ORDER BY version")?;
let rows = stmt.all(SqliteParams::none())?;
rows.into_iter()
.map(|mut row| {
let version = row
.remove("version")
.and_then(|value| value.as_i64())
.ok_or_else(|| SqliteError::new("schema_versions.version was missing"))?;
let applied_at = row
.remove("applied_at")
.and_then(|value| value.as_i64())
.ok_or_else(|| SqliteError::new("schema_versions.applied_at was missing"))?;
let description = row
.remove("description")
.and_then(SqliteValue::into_string_lossy);
Ok(MigrationHistoryEntry {
version,
applied_at,
description,
})
})
.collect()
}
pub fn current_time_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as i64)
.unwrap_or(0)
}