Complete TUI Phase 3 implementation with all 5 power feature screens: - Who screen: 5 modes (expert/workload/reviews/active/overlap) with mode tabs, input bar, result rendering, and hint bar - Search screen: full-text search with result list and scoring display - Timeline screen: chronological event feed with time-relative display - Trace screen: file provenance chains with expand/collapse, rename tracking, and linked issues/discussions - File History screen: per-file MR timeline with rename chain display and discussion snippets Also includes: - Command palette overlay (fuzzy search) - Bootstrap screen (initial sync flow) - Action layer split from monolithic action.rs to per-screen modules - Entity and render cache infrastructure - Shared who_types module in core crate - All screens wired into view/mod.rs dispatch - 597 tests passing, clippy clean (pedantic + nursery), fmt clean
299 lines
10 KiB
Rust
299 lines
10 KiB
Rust
#![allow(dead_code)]
|
|
|
|
use anyhow::{Context, Result};
|
|
use rusqlite::Connection;
|
|
|
|
use crate::state::bootstrap::{DataReadiness, SchemaCheck};
|
|
|
|
/// Minimum schema version required by this TUI version.
|
|
pub const MINIMUM_SCHEMA_VERSION: i32 = 20;
|
|
|
|
/// Check the schema version of the database.
|
|
///
|
|
/// Returns [`SchemaCheck::NoDB`] if the `schema_version` table doesn't exist,
|
|
/// [`SchemaCheck::Incompatible`] if the version is below the minimum,
|
|
/// or [`SchemaCheck::Compatible`] if all is well.
|
|
pub fn check_schema_version(conn: &Connection, minimum: i32) -> SchemaCheck {
|
|
// Check if schema_version table exists.
|
|
let table_exists: bool = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_version'",
|
|
[],
|
|
|r| r.get::<_, i64>(0),
|
|
)
|
|
.map(|c| c > 0)
|
|
.unwrap_or(false);
|
|
|
|
if !table_exists {
|
|
return SchemaCheck::NoDB;
|
|
}
|
|
|
|
// Read the current version.
|
|
match conn.query_row("SELECT version FROM schema_version LIMIT 1", [], |r| {
|
|
r.get::<_, i32>(0)
|
|
}) {
|
|
Ok(version) if version >= minimum => SchemaCheck::Compatible { version },
|
|
Ok(found) => SchemaCheck::Incompatible { found, minimum },
|
|
Err(_) => SchemaCheck::NoDB,
|
|
}
|
|
}
|
|
|
|
/// Check whether the database has enough data to skip the bootstrap screen.
|
|
///
|
|
/// Counts issues, merge requests, and search documents. The `documents` table
|
|
/// may not exist on older schemas, so its absence is treated as "no documents."
|
|
pub fn check_data_readiness(conn: &Connection) -> Result<DataReadiness> {
|
|
let has_issues: bool = conn
|
|
.query_row("SELECT EXISTS(SELECT 1 FROM issues LIMIT 1)", [], |r| {
|
|
r.get(0)
|
|
})
|
|
.context("checking issues")?;
|
|
|
|
let has_mrs: bool = conn
|
|
.query_row(
|
|
"SELECT EXISTS(SELECT 1 FROM merge_requests LIMIT 1)",
|
|
[],
|
|
|r| r.get(0),
|
|
)
|
|
.context("checking merge requests")?;
|
|
|
|
// documents table may not exist yet (created by generate-docs).
|
|
let has_documents: bool = conn
|
|
.query_row("SELECT EXISTS(SELECT 1 FROM documents LIMIT 1)", [], |r| {
|
|
r.get(0)
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
let schema_version = conn
|
|
.query_row("SELECT version FROM schema_version LIMIT 1", [], |r| {
|
|
r.get::<_, i32>(0)
|
|
})
|
|
.unwrap_or(0);
|
|
|
|
Ok(DataReadiness {
|
|
has_issues,
|
|
has_mrs,
|
|
has_documents,
|
|
schema_version,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Create the minimal schema needed for bootstrap / data-readiness queries.
|
|
fn create_dashboard_schema(conn: &Connection) {
|
|
conn.execute_batch(
|
|
"
|
|
CREATE TABLE projects (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_project_id INTEGER UNIQUE NOT NULL,
|
|
path_with_namespace TEXT NOT NULL
|
|
);
|
|
CREATE TABLE issues (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER UNIQUE NOT NULL,
|
|
project_id INTEGER NOT NULL,
|
|
iid INTEGER NOT NULL,
|
|
title TEXT,
|
|
state TEXT NOT NULL,
|
|
author_username TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE merge_requests (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER UNIQUE NOT NULL,
|
|
project_id INTEGER NOT NULL,
|
|
iid INTEGER NOT NULL,
|
|
title TEXT,
|
|
state TEXT,
|
|
author_username TEXT,
|
|
created_at INTEGER,
|
|
updated_at INTEGER,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE discussions (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_discussion_id TEXT NOT NULL,
|
|
project_id INTEGER NOT NULL,
|
|
noteable_type TEXT NOT NULL,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE notes (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER UNIQUE NOT NULL,
|
|
discussion_id INTEGER NOT NULL,
|
|
project_id INTEGER NOT NULL,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
author_username TEXT,
|
|
body TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE documents (
|
|
id INTEGER PRIMARY KEY,
|
|
source_type TEXT NOT NULL,
|
|
source_id INTEGER NOT NULL,
|
|
project_id INTEGER NOT NULL,
|
|
content_text TEXT NOT NULL,
|
|
content_hash TEXT NOT NULL
|
|
);
|
|
CREATE TABLE embedding_metadata (
|
|
document_id INTEGER NOT NULL,
|
|
chunk_index INTEGER NOT NULL DEFAULT 0,
|
|
model TEXT NOT NULL,
|
|
dims INTEGER NOT NULL,
|
|
document_hash TEXT NOT NULL,
|
|
chunk_hash TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY(document_id, chunk_index)
|
|
);
|
|
CREATE TABLE sync_runs (
|
|
id INTEGER PRIMARY KEY,
|
|
started_at INTEGER NOT NULL,
|
|
heartbeat_at INTEGER NOT NULL,
|
|
finished_at INTEGER,
|
|
status TEXT NOT NULL,
|
|
command TEXT NOT NULL,
|
|
error TEXT
|
|
);
|
|
",
|
|
)
|
|
.expect("create dashboard schema");
|
|
}
|
|
|
|
fn insert_issue(conn: &Connection, iid: i64, state: &str, updated_at: i64) {
|
|
conn.execute(
|
|
"INSERT INTO issues (gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (?1, 1, ?2, ?3, ?4, ?5, ?5, ?5)",
|
|
rusqlite::params![iid * 100, iid, format!("Issue {iid}"), state, updated_at],
|
|
)
|
|
.expect("insert issue");
|
|
}
|
|
|
|
fn insert_mr(conn: &Connection, iid: i64, state: &str, updated_at: i64) {
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (?1, 1, ?2, ?3, ?4, ?5, ?5, ?5)",
|
|
rusqlite::params![iid * 100 + 50, iid, format!("MR {iid}"), state, updated_at],
|
|
)
|
|
.expect("insert mr");
|
|
}
|
|
|
|
/// TDD anchor test from bead spec.
|
|
#[test]
|
|
fn test_schema_preflight_rejects_old() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch(
|
|
"CREATE TABLE schema_version (version INTEGER);
|
|
INSERT INTO schema_version (version) VALUES (1);",
|
|
)
|
|
.unwrap();
|
|
|
|
let result = check_schema_version(&conn, 20);
|
|
assert!(matches!(
|
|
result,
|
|
SchemaCheck::Incompatible {
|
|
found: 1,
|
|
minimum: 20
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_preflight_accepts_compatible() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch(
|
|
"CREATE TABLE schema_version (version INTEGER);
|
|
INSERT INTO schema_version (version) VALUES (26);",
|
|
)
|
|
.unwrap();
|
|
|
|
let result = check_schema_version(&conn, 20);
|
|
assert!(matches!(result, SchemaCheck::Compatible { version: 26 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_preflight_exact_minimum() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch(
|
|
"CREATE TABLE schema_version (version INTEGER);
|
|
INSERT INTO schema_version (version) VALUES (20);",
|
|
)
|
|
.unwrap();
|
|
|
|
let result = check_schema_version(&conn, 20);
|
|
assert!(matches!(result, SchemaCheck::Compatible { version: 20 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_preflight_no_db() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
let result = check_schema_version(&conn, 20);
|
|
assert!(matches!(result, SchemaCheck::NoDB));
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_preflight_empty_schema_version_table() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch("CREATE TABLE schema_version (version INTEGER);")
|
|
.unwrap();
|
|
|
|
let result = check_schema_version(&conn, 20);
|
|
assert!(matches!(result, SchemaCheck::NoDB));
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_data_readiness_empty() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
create_dashboard_schema(&conn);
|
|
conn.execute_batch(
|
|
"CREATE TABLE schema_version (version INTEGER);
|
|
INSERT INTO schema_version (version) VALUES (26);",
|
|
)
|
|
.unwrap();
|
|
|
|
let readiness = check_data_readiness(&conn).unwrap();
|
|
assert!(!readiness.has_issues);
|
|
assert!(!readiness.has_mrs);
|
|
assert!(!readiness.has_documents);
|
|
assert_eq!(readiness.schema_version, 26);
|
|
assert!(!readiness.has_any_data());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_data_readiness_with_data() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
create_dashboard_schema(&conn);
|
|
conn.execute_batch(
|
|
"CREATE TABLE schema_version (version INTEGER);
|
|
INSERT INTO schema_version (version) VALUES (26);",
|
|
)
|
|
.unwrap();
|
|
|
|
insert_issue(&conn, 1, "opened", 1_700_000_000_000);
|
|
insert_mr(&conn, 1, "merged", 1_700_000_000_000);
|
|
|
|
let readiness = check_data_readiness(&conn).unwrap();
|
|
assert!(readiness.has_issues);
|
|
assert!(readiness.has_mrs);
|
|
assert!(!readiness.has_documents);
|
|
assert_eq!(readiness.schema_version, 26);
|
|
assert!(readiness.has_any_data());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_data_readiness_documents_table_missing() {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
create_dashboard_schema(&conn);
|
|
// No documents table — should still work.
|
|
|
|
let readiness = check_data_readiness(&conn).unwrap();
|
|
assert!(!readiness.has_documents);
|
|
}
|
|
}
|