claw_branch/commit/
validator.rs1use sqlx::{
4 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
5 SqlitePool,
6};
7
8use crate::{
9 commit::cherry::CherryPick,
10 error::BranchResult,
11 types::{Branch, BranchStatus},
12};
13
14pub struct CommitValidator {
22 pool: SqlitePool,
23}
24
25#[derive(Debug, Clone)]
27pub struct ValidationReport {
28 pub ok: bool,
30 pub violations: Vec<String>,
32 pub warnings: Vec<String>,
34}
35
36impl CommitValidator {
37 pub fn new(pool: SqlitePool) -> Self {
39 Self { pool }
40 }
41
42 pub async fn from_branch(branch: &Branch) -> BranchResult<Self> {
44 let pool = SqlitePoolOptions::new()
45 .max_connections(1)
46 .connect_with(
47 SqliteConnectOptions::new()
48 .filename(&branch.db_path)
49 .create_if_missing(false)
50 .read_only(true)
51 .journal_mode(SqliteJournalMode::Wal),
52 )
53 .await?;
54 Ok(Self { pool })
55 }
56
57 pub async fn validate(
65 &self,
66 cherry: &CherryPick,
67 source: &Branch,
68 target: &Branch,
69 ) -> BranchResult<ValidationReport> {
70 let mut violations: Vec<String> = Vec::new();
71 let mut warnings: Vec<String> = Vec::new();
72
73 match &source.status {
75 BranchStatus::Active | BranchStatus::Dormant => {}
76 other => violations.push(format!(
77 "source branch {} is {:?}, must be Active or Dormant",
78 source.id,
79 other.kind()
80 )),
81 }
82 if !matches!(&target.status, BranchStatus::Active) {
83 violations.push(format!(
84 "target branch {} is {:?}, must be Active",
85 target.id,
86 target.status.kind()
87 ));
88 }
89
90 for sel in &cherry.entity_selections {
92 if sel.entity_ids.is_empty() {
93 continue; }
95 let table = sel.entity_type.table_name();
96 let table_exists: bool = sqlx::query_scalar::<_, i64>(
97 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?",
98 )
99 .bind(table)
100 .fetch_one(&self.pool)
101 .await
102 .map(|n| n > 0)
103 .unwrap_or(false);
104
105 if !table_exists {
106 warnings.push(format!(
107 "table '{table}' does not exist in source; skipping entity existence check"
108 ));
109 continue;
110 }
111
112 for entity_id in &sel.entity_ids {
113 let exists: bool = sqlx::query_scalar::<_, i64>(&format!(
114 "SELECT COUNT(*) FROM {table} WHERE id = ?"
115 ))
116 .bind(entity_id)
117 .fetch_one(&self.pool)
118 .await
119 .map(|n| n > 0)
120 .unwrap_or(false);
121
122 if !exists {
123 violations.push(format!(
124 "entity '{}' of type {:?} not found in source branch {}",
125 entity_id, sel.entity_type, source.id
126 ));
127 }
128 }
129 }
130
131 let ok = violations.is_empty();
132 Ok(ValidationReport {
133 ok,
134 violations,
135 warnings,
136 })
137 }
138}