Skip to main content

gen_models/
operations.rs

1use std::{
2    collections::HashMap,
3    convert::TryInto,
4    io::{self, BufReader},
5    path::{Path, PathBuf},
6    rc::Rc,
7    string::ToString,
8};
9
10use gen_core::{HashId, Workspace, calculate_hash, traits::Capnp};
11use gen_graph::{OperationGraph, all_simple_paths};
12use petgraph::{Direction, graphmap::UnGraphMap};
13use rusqlite::{Result as SQLResult, Row, params, types::Value};
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256};
16use thiserror::Error;
17
18use crate::{
19    changesets::{
20        DatabaseChangeset, get_changeset_dependencies_from_path, get_changeset_from_path,
21    },
22    db::OperationsConnection,
23    errors::{BranchError, FileAdditionError, RemoteError},
24    file_types::FileTypes,
25    gen_models_capnp::operation,
26    session_operations::DependencyModels,
27    traits::*,
28};
29
30#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
31pub struct Operation {
32    pub hash: HashId,
33    pub parent_hash: Option<HashId>,
34    pub change_type: String,
35    pub created_on: i64,
36}
37
38impl<'a> Capnp<'a> for Operation {
39    type Builder = operation::Builder<'a>;
40    type Reader = operation::Reader<'a>;
41
42    fn write_capnp(&self, builder: &mut Self::Builder) {
43        builder.set_hash(&self.hash.0).unwrap();
44        match &self.parent_hash {
45            None => {
46                builder.reborrow().get_parent_hash().set_none(());
47            }
48            Some(n) => {
49                builder.reborrow().get_parent_hash().set_some(&n.0).unwrap();
50            }
51        }
52        builder.set_change_type(&self.change_type);
53        builder.set_created_on(self.created_on);
54    }
55
56    fn read_capnp(reader: Self::Reader) -> Self {
57        let hash = reader
58            .get_hash()
59            .unwrap()
60            .as_slice()
61            .unwrap()
62            .try_into()
63            .unwrap();
64        let parent_hash = match reader.get_parent_hash().which().unwrap() {
65            operation::parent_hash::None(()) => None,
66            operation::parent_hash::Some(n) => {
67                Some(n.unwrap().as_slice().unwrap().try_into().unwrap())
68            }
69        };
70        let change_type = reader.get_change_type().unwrap().to_string().unwrap();
71        let created_on = reader.get_created_on();
72
73        Operation {
74            hash,
75            parent_hash,
76            change_type,
77            created_on,
78        }
79    }
80}
81
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83pub struct HashRange {
84    pub from: Option<HashId>,
85    pub to: Option<HashId>,
86}
87
88#[derive(Debug, Error)]
89pub enum HashParseError {
90    #[error("No current branch is checked out.")]
91    NoCurrentBranch,
92    #[error("Branch '{0}' not found.")]
93    BranchNotFound(String),
94    #[error("Branch '{0}' has no operations.")]
95    EmptyBranch(String),
96    #[error("Reference '{0}' is not a valid HEAD shorthand.")]
97    InvalidHead(String),
98    #[error("HEAD offset {0} is out of range for the current branch.")]
99    HeadOffsetOutOfRange(usize),
100    #[error("Reference '{0}' did not match any operation.")]
101    OperationNotFound(String),
102    #[error("Reference '{0}' matches multiple operations.")]
103    OperationAmbiguous(String),
104}
105
106impl Operation {
107    pub fn create(
108        conn: &OperationsConnection,
109        change_type: &str,
110        hash: &HashId,
111    ) -> SQLResult<Operation> {
112        let current_op = OperationState::get_operation(conn);
113        let current_branch_id =
114            OperationState::get_current_branch(conn).expect("No branch is checked out.");
115
116        let timestamp = chrono::Utc::now().timestamp_nanos_opt().unwrap();
117        let query = "INSERT INTO operations (hash, change_type, parent_hash, created_on) VALUES (?1, ?2, ?3, ?4);";
118        let mut stmt = conn.prepare(query).unwrap();
119        stmt.execute(params![hash, change_type, current_op, timestamp])?;
120        let operation = Operation {
121            hash: *hash,
122            parent_hash: current_op,
123            change_type: change_type.to_string(),
124            created_on: timestamp,
125        };
126        // TODO: error condition here where we can write to disk but transaction fails
127        OperationState::set_operation(conn, &operation.hash);
128        Branch::set_current_operation(conn, current_branch_id, &operation.hash);
129        Ok(operation)
130    }
131
132    pub fn create_without_tracking(
133        conn: &OperationsConnection,
134        hash: &HashId,
135        change_type: &str,
136        parent_hash: Option<HashId>,
137        created_on: Option<i64>,
138    ) -> SQLResult<Operation> {
139        let timestamp = created_on.unwrap_or(chrono::Utc::now().timestamp_nanos_opt().unwrap());
140        let query = "INSERT INTO operations (hash, change_type, parent_hash, created_on) VALUES (?1, ?2, ?3, ?4);";
141        let mut stmt = conn.prepare(query).unwrap();
142        stmt.execute(params![hash, change_type, parent_hash, timestamp])?;
143        let operation = Operation {
144            hash: *hash,
145            parent_hash,
146            change_type: change_type.to_string(),
147            created_on: timestamp,
148        };
149        Ok(operation)
150    }
151
152    pub fn add_file(
153        conn: &OperationsConnection,
154        operation_hash: &HashId,
155        file_addition_id: &HashId,
156    ) -> SQLResult<()> {
157        let query =
158            "INSERT INTO operation_files (operation_hash, file_addition_id) VALUES (?1, ?2)";
159        let mut stmt = conn.prepare(query).unwrap();
160        stmt.execute(params![operation_hash, file_addition_id])?;
161        Ok(())
162    }
163
164    pub fn add_database(
165        conn: &OperationsConnection,
166        operation_hash: &HashId,
167        db_uuid: &str,
168    ) -> SQLResult<()> {
169        let query =
170            "INSERT INTO operation_databases (operation_hash, database_uuid) VALUES (?1, ?2)";
171        let mut stmt = conn.prepare(query).unwrap();
172        stmt.execute(params![operation_hash, db_uuid])?;
173        Ok(())
174    }
175
176    pub fn get_upstream(conn: &OperationsConnection, operation_hash: &HashId) -> Vec<HashId> {
177        let query = "WITH RECURSIVE r_operations(operation_hash, depth) AS ( \
178        select ?1, 0 UNION \
179        select parent_hash, depth + 1 from r_operations join operations ON hash=operation_hash \
180        ) SELECT operation_hash, depth from r_operations where operation_hash is not null order by depth desc;";
181        let mut stmt = conn.prepare(query).unwrap();
182        stmt.query_map([operation_hash], |row| row.get(0))
183            .unwrap()
184            .map(|id| id.unwrap())
185            .collect::<Vec<HashId>>()
186    }
187
188    pub fn get_operation_graph(conn: &OperationsConnection) -> OperationGraph {
189        let mut graph = OperationGraph::new();
190        let operations = Operation::query(conn, "select * from operations;", rusqlite::params![]);
191        for op in operations.iter() {
192            graph.add_node(op.hash);
193            if let Some(v) = op.parent_hash {
194                graph.add_node(v);
195                graph.add_edge(v, op.hash, ());
196            }
197        }
198        graph
199    }
200
201    pub fn get_path_between(
202        conn: &OperationsConnection,
203        source_node: HashId,
204        target_node: HashId,
205    ) -> Vec<(HashId, Direction, HashId)> {
206        let directed_graph = Operation::get_operation_graph(conn);
207        let mut undirected_graph: UnGraphMap<HashId, ()> = Default::default();
208
209        for node in directed_graph.nodes() {
210            undirected_graph.add_node(node);
211        }
212        for (source, target, _weight) in directed_graph.all_edges() {
213            undirected_graph.add_edge(source, target, ());
214        }
215        let mut patch_path: Vec<(HashId, Direction, HashId)> = vec![];
216        for path in all_simple_paths(&undirected_graph, source_node, target_node) {
217            let mut last_node = source_node;
218            for node in &path[1..] {
219                if *node != source_node {
220                    for (_edge_src, edge_target, _edge_weight) in
221                        directed_graph.edges_directed(last_node, Direction::Outgoing)
222                    {
223                        if edge_target == *node {
224                            patch_path.push((last_node, Direction::Outgoing, *node));
225                            break;
226                        }
227                    }
228                    for (edge_src, _edge_target, _edge_weight) in
229                        directed_graph.edges_directed(last_node, Direction::Incoming)
230                    {
231                        if edge_src == *node {
232                            patch_path.push((last_node, Direction::Incoming, *node));
233                            break;
234                        }
235                    }
236                }
237                last_node = *node;
238            }
239        }
240        patch_path
241    }
242
243    pub fn search_hash(
244        conn: &OperationsConnection,
245        op_hash: &str,
246    ) -> Result<Operation, HashParseError> {
247        let matches = Operation::search_hashes(conn, op_hash);
248        match matches.len() {
249            0 => Err(HashParseError::OperationNotFound(op_hash.to_string())),
250            1 => Ok(matches[0].clone()),
251            _ => Err(HashParseError::OperationAmbiguous(op_hash.to_string())),
252        }
253    }
254
255    pub fn search_hashes(conn: &OperationsConnection, op_hash: &str) -> Vec<Operation> {
256        Operation::query(
257            conn,
258            "select * from operations where hex(hash) LIKE ?1",
259            params![format!("{op_hash}%")],
260        )
261    }
262
263    pub fn get_changeset_path(&self, workspace: &Workspace) -> PathBuf {
264        workspace.changeset_path(&self.hash).join("changeset")
265    }
266
267    pub fn get_changeset_dependencies_path(&self, workspace: &Workspace) -> PathBuf {
268        workspace.changeset_path(&self.hash).join("dependencies")
269    }
270
271    pub fn get_changeset(&self, workspace: &Workspace) -> DatabaseChangeset {
272        let path = self.get_changeset_path(workspace);
273        get_changeset_from_path(path)
274    }
275
276    pub fn get_changeset_dependencies(&self, workspace: &Workspace) -> DependencyModels {
277        let path = self.get_changeset_dependencies_path(workspace);
278        get_changeset_dependencies_from_path(path)
279    }
280}
281
282pub fn parse_hash(conn: &OperationsConnection, input: &str) -> Result<HashRange, HashParseError> {
283    if input.contains("..") {
284        let mut it = input.split("..");
285        let from_ref = it.next().unwrap_or_default();
286        let to_ref = it.next().unwrap_or_default();
287        return Ok(HashRange {
288            from: Some(resolve_reference(conn, from_ref)?),
289            to: Some(resolve_reference(conn, to_ref)?),
290        });
291    }
292
293    Ok(HashRange {
294        from: None,
295        to: Some(resolve_reference(conn, input)?),
296    })
297}
298
299fn resolve_reference(
300    conn: &OperationsConnection,
301    reference: &str,
302) -> Result<HashId, HashParseError> {
303    if reference.starts_with("HEAD") {
304        return resolve_head(conn, reference);
305    }
306
307    if let Some(branch) = Branch::get_by_name(conn, reference) {
308        if let Some(hash) = branch.current_operation_hash {
309            return Ok(hash);
310        }
311        return Err(HashParseError::EmptyBranch(branch.name));
312    }
313
314    let operation = Operation::search_hash(conn, reference)?;
315    Ok(operation.hash)
316}
317
318fn resolve_head(conn: &OperationsConnection, reference: &str) -> Result<HashId, HashParseError> {
319    let branch_id =
320        OperationState::get_current_branch(conn).ok_or(HashParseError::NoCurrentBranch)?;
321    let branch = Branch::get_by_id(conn, branch_id)
322        .ok_or_else(|| HashParseError::BranchNotFound(branch_id.to_string()))?;
323    let operations = Branch::get_operations(conn, branch.id);
324    if operations.is_empty() {
325        return Err(HashParseError::EmptyBranch(branch.name));
326    }
327    if reference == "HEAD" {
328        return Ok(operations.last().unwrap().hash);
329    }
330    if let Some(offset) = reference.strip_prefix("HEAD~") {
331        let offset: usize = offset
332            .parse()
333            .map_err(|_| HashParseError::InvalidHead(reference.to_string()))?;
334        let head_index = operations.len() - 1;
335        let target_index = head_index
336            .checked_sub(offset)
337            .ok_or(HashParseError::HeadOffsetOutOfRange(offset))?;
338        return Ok(operations[target_index].hash);
339    }
340
341    Err(HashParseError::InvalidHead(reference.to_string()))
342}
343
344impl Query for Operation {
345    type Model = Operation;
346
347    const PRIMARY_KEY: &'static str = "hash";
348    const TABLE_NAME: &'static str = "operations";
349
350    fn process_row(row: &Row) -> Self::Model {
351        Operation {
352            hash: row.get(0).unwrap(),
353            parent_hash: row.get(1).unwrap(),
354            change_type: row.get(2).unwrap(),
355            created_on: row.get(3).unwrap(),
356        }
357    }
358}
359
360pub struct OperationFile {
361    pub file_path: String,
362    pub file_type: FileTypes,
363}
364
365pub struct OperationInfo {
366    pub files: Vec<OperationFile>,
367    pub description: String,
368}
369
370pub fn calculate_file_checksum<P: AsRef<Path>>(file_path: P) -> Result<HashId, std::io::Error> {
371    let file = std::fs::File::open(file_path)?;
372    let reader = BufReader::new(file);
373    let hash_bytes = calculate_stream_hash(reader)?;
374    Ok(HashId(hash_bytes))
375}
376
377fn calculate_stream_hash<R: std::io::Read>(mut reader: R) -> Result<[u8; 32], std::io::Error> {
378    let mut hasher = Sha256::new();
379    io::copy(&mut reader, &mut hasher)?;
380    let result = hasher.finalize();
381    let mut hash_array = [0u8; 32];
382    hash_array.copy_from_slice(&result);
383    Ok(hash_array)
384}
385
386#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
387pub struct FileAddition {
388    pub id: HashId,
389    pub file_path: String,
390    pub file_type: FileTypes,
391    pub checksum: HashId,
392}
393
394impl Query for FileAddition {
395    type Model = FileAddition;
396
397    const TABLE_NAME: &'static str = "file_additions";
398
399    fn process_row(row: &Row) -> Self::Model {
400        Self::Model {
401            id: row.get(0).unwrap(),
402            file_path: row.get(1).unwrap(),
403            file_type: row.get(2).unwrap(),
404            checksum: row.get(3).unwrap(),
405        }
406    }
407}
408
409impl FileAddition {
410    pub fn generate_file_addition_id(checksum: &HashId, file_path: &str) -> HashId {
411        let combined = format!("{checksum};{file_path}");
412        HashId(calculate_hash(&combined))
413    }
414
415    fn normalize_file_paths(workspace: &Workspace, file_path: &str) -> (String, String) {
416        if file_path.is_empty() {
417            return (String::new(), String::new());
418        }
419        let repo_root = workspace.repo_root().unwrap();
420
421        let provided_path = Path::new(file_path);
422
423        if provided_path.is_absolute() {
424            if provided_path.starts_with(&repo_root) {
425                let absolute = provided_path.to_string_lossy().to_string();
426                let relative = provided_path
427                    .strip_prefix(&repo_root)
428                    .unwrap()
429                    .to_string_lossy()
430                    .to_string();
431                return (absolute, relative);
432            }
433        } else {
434            let absolute = repo_root.join(provided_path);
435            if absolute.exists() {
436                let relative = absolute
437                    .strip_prefix(&repo_root)
438                    .unwrap()
439                    .to_string_lossy()
440                    .to_string();
441                return (absolute.to_string_lossy().to_string(), relative);
442            }
443        };
444
445        let fallback = file_path.to_string();
446        (fallback.clone(), fallback)
447    }
448
449    pub fn get_or_create(
450        workspace: &Workspace,
451        conn: &OperationsConnection,
452        file_path: &str,
453        file_type: FileTypes,
454        checksum_override: Option<HashId>,
455    ) -> Result<FileAddition, FileAdditionError> {
456        let (absolute_file_path, relative_file_path) =
457            FileAddition::normalize_file_paths(workspace, file_path);
458
459        // TODO: Verify checksum_override actually matches the file's checksum?
460        let checksum = if let Some(checksum_override) = checksum_override {
461            checksum_override
462        } else {
463            let absolute_path = Path::new(&absolute_file_path);
464            let checksum_path = if absolute_path.is_file() {
465                absolute_file_path.as_str()
466            } else {
467                relative_file_path.as_str()
468            };
469            match calculate_file_checksum(checksum_path) {
470                Ok(checksum) => checksum,
471                Err(e) => match e.kind() {
472                    std::io::ErrorKind::NotFound => HashId::convert_str("non-existent"),
473                    std::io::ErrorKind::PermissionDenied => {
474                        return Err(FileAdditionError::FilePermissionDenied(
475                            file_path.to_string(),
476                        ));
477                    }
478                    _ => {
479                        return Err(FileAdditionError::FileReadError(e));
480                    }
481                },
482            }
483        };
484
485        let id = FileAddition::generate_file_addition_id(&checksum, &relative_file_path);
486
487        let query = "INSERT INTO file_additions (id, file_path, file_type, checksum) VALUES (?1, ?2, ?3, ?4);";
488        let mut stmt = conn.prepare(query).unwrap();
489
490        let addition = FileAddition {
491            id,
492            file_path: relative_file_path.clone(),
493            file_type,
494            checksum,
495        };
496
497        match stmt.execute((&id, &relative_file_path, file_type, &checksum)) {
498            Ok(_) => Ok(addition),
499            Err(err) => match &err {
500                rusqlite::Error::SqliteFailure(suberr, _details) => {
501                    if suberr.code == rusqlite::ErrorCode::ConstraintViolation {
502                        Ok(addition)
503                    } else {
504                        Err(FileAdditionError::DatabaseError(err))
505                    }
506                }
507                _ => Err(FileAdditionError::DatabaseError(err)),
508            },
509        }
510    }
511
512    pub fn get_files_for_operation(
513        conn: &OperationsConnection,
514        operation_hash: &HashId,
515    ) -> Vec<FileAddition> {
516        let query = "select fa.* from file_additions fa left join operation_files of on (fa.id = of.file_addition_id) where of.operation_hash = ?1";
517        let mut stmt = conn.prepare(query).unwrap();
518        let rows = stmt
519            .query_map(params![operation_hash], |row| {
520                Ok(FileAddition::process_row(row))
521            })
522            .unwrap();
523        rows.map(|row| row.unwrap()).collect()
524    }
525
526    pub fn query_by_operations(
527        conn: &OperationsConnection,
528        operations: &[HashId],
529    ) -> Result<HashMap<HashId, Vec<FileAddition>>, FileAdditionError> {
530        let query = "select fa.*, of.operation_hash from file_additions fa left join operation_files of on (fa.id = of.file_addition_id) where of.operation_hash in rarray(?1)";
531        let mut stmt = conn.prepare(query).unwrap();
532        let rows = stmt
533            .query_map(
534                params![Rc::new(
535                    operations
536                        .iter()
537                        .map(|h| Value::from(*h))
538                        .collect::<Vec<Value>>()
539                )],
540                |row| Ok((FileAddition::process_row(row), row.get::<_, HashId>(4)?)),
541            )
542            .unwrap();
543        rows.into_iter()
544            .try_fold(HashMap::new(), |mut acc: HashMap<_, Vec<_>>, row| {
545                let (item, hash) = row?;
546                acc.entry(hash).or_default().push(item);
547                Ok(acc)
548            })
549    }
550
551    pub fn hashed_filename(self) -> String {
552        format!(
553            "{}.{}",
554            self.checksum.clone(),
555            &FileTypes::suffix(self.file_type)
556        )
557    }
558}
559
560#[derive(Debug, Error)]
561pub enum OperationSummaryError {
562    #[error("Database error: {0}")]
563    DatabaseError(#[from] rusqlite::Error),
564}
565
566#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
567pub struct OperationSummary {
568    pub id: i64,
569    pub operation_hash: HashId,
570    pub summary: String,
571}
572
573impl Query for OperationSummary {
574    type Model = OperationSummary;
575
576    const TABLE_NAME: &'static str = "operation_summaries";
577
578    fn process_row(row: &Row) -> Self::Model {
579        Self::Model {
580            id: row.get(0).unwrap(),
581            operation_hash: row.get(1).unwrap(),
582            summary: row.get(2).unwrap(),
583        }
584    }
585}
586
587impl OperationSummary {
588    pub fn create(
589        conn: &OperationsConnection,
590        operation_hash: &HashId,
591        summary: &str,
592    ) -> OperationSummary {
593        let query = "INSERT INTO operation_summaries (operation_hash, summary) VALUES (?1, ?2) RETURNING (id)";
594        let mut stmt = conn.prepare(query).unwrap();
595        let mut rows = stmt
596            .query_map(params![operation_hash, summary], |row| {
597                Ok(OperationSummary {
598                    id: row.get(0)?,
599                    operation_hash: *operation_hash,
600                    summary: summary.to_string(),
601                })
602            })
603            .unwrap();
604        rows.next().unwrap().unwrap()
605    }
606
607    pub fn set_message(conn: &OperationsConnection, id: i64, message: &str) -> SQLResult<()> {
608        let query = "UPDATE operation_summaries SET summary = ?2 where id = ?1";
609        let mut stmt = conn.prepare(query).unwrap();
610        stmt.execute(params![id, message])?;
611        Ok(())
612    }
613
614    pub fn query_by_operations(
615        conn: &OperationsConnection,
616        operations: &[HashId],
617    ) -> Result<HashMap<HashId, Vec<Self>>, OperationSummaryError> {
618        let query = "select * from operation_summaries where operation_hash in rarray(?1)";
619        let mut stmt = conn.prepare(query).unwrap();
620        let rows = stmt
621            .query_map(
622                params![Rc::new(
623                    operations
624                        .iter()
625                        .map(|h| Value::from(*h))
626                        .collect::<Vec<Value>>()
627                )],
628                |row| Ok(Self::process_row(row)),
629            )
630            .unwrap();
631        rows.into_iter()
632            .try_fold(HashMap::new(), |mut acc: HashMap<_, Vec<_>>, row| {
633                let item = row?;
634                acc.entry(item.operation_hash).or_default().push(item);
635                Ok(acc)
636            })
637    }
638}
639
640impl<'a> Capnp<'a> for FileAddition {
641    type Builder = crate::gen_models_capnp::file_addition::Builder<'a>;
642    type Reader = crate::gen_models_capnp::file_addition::Reader<'a>;
643
644    fn write_capnp(&self, builder: &mut Self::Builder) {
645        builder.set_id(&self.id.0).unwrap();
646        builder.set_file_path(&self.file_path);
647        builder.set_file_type(self.file_type.into());
648        builder.set_checksum(&self.checksum.0).unwrap();
649    }
650
651    fn read_capnp(reader: Self::Reader) -> Self {
652        Self {
653            id: reader
654                .get_id()
655                .unwrap()
656                .as_slice()
657                .unwrap()
658                .try_into()
659                .unwrap(),
660            file_path: reader.get_file_path().unwrap().to_string().unwrap(),
661            file_type: reader.get_file_type().unwrap().into(),
662            checksum: reader
663                .get_checksum()
664                .unwrap()
665                .as_slice()
666                .unwrap()
667                .try_into()
668                .unwrap(),
669        }
670    }
671}
672
673impl<'a> Capnp<'a> for OperationSummary {
674    type Builder = crate::gen_models_capnp::operation_summary::Builder<'a>;
675    type Reader = crate::gen_models_capnp::operation_summary::Reader<'a>;
676
677    fn write_capnp(&self, builder: &mut Self::Builder) {
678        builder.set_id(self.id);
679        builder.set_operation_hash(&self.operation_hash.0).unwrap();
680        builder.set_summary(&self.summary);
681    }
682
683    fn read_capnp(reader: Self::Reader) -> Self {
684        Self {
685            id: reader.get_id(),
686            operation_hash: reader
687                .get_operation_hash()
688                .unwrap()
689                .as_slice()
690                .unwrap()
691                .try_into()
692                .unwrap(),
693            summary: reader.get_summary().unwrap().to_string().unwrap(),
694        }
695    }
696}
697
698#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
699pub struct Remote {
700    pub name: String,
701    pub url: String,
702}
703
704impl Query for Remote {
705    type Model = Remote;
706
707    const TABLE_NAME: &'static str = "remotes";
708
709    fn process_row(row: &Row) -> Self::Model {
710        Remote {
711            name: row.get(0).unwrap(),
712            url: row.get(1).unwrap(),
713        }
714    }
715}
716
717impl Remote {
718    /// Validate remote name - no spaces or special characters except hyphens and underscores
719    pub fn validate_name(name: &str) -> Result<(), RemoteError> {
720        if name.is_empty() {
721            return Err(RemoteError::EmptyName);
722        }
723
724        if name
725            .chars()
726            .any(|c| !c.is_alphanumeric() && c != '-' && c != '_')
727        {
728            return Err(RemoteError::InvalidNameCharacters);
729        }
730
731        Ok(())
732    }
733
734    /// Validate URL format
735    pub fn validate_url(url: &str) -> Result<(), RemoteError> {
736        if url.is_empty() {
737            return Err(RemoteError::EmptyUrl);
738        }
739
740        // Check if it looks like a URL with a scheme
741        if url.contains("://") {
742            match url::Url::parse(url) {
743                Ok(parsed_url) => {
744                    // Only allow http, https, and ssh schemes
745                    match parsed_url.scheme() {
746                        "http" | "https" | "ssh" | "file" => Ok(()),
747                        _ => Err(RemoteError::UnsupportedUrlScheme),
748                    }
749                }
750                Err(_) => Err(RemoteError::InvalidUrl("Invalid URL format".to_string())),
751            }
752        } else if url.starts_with('/') || url.contains(':') {
753            // Assume it's a file path or SSH-style path (like user@host:path)
754            Ok(())
755        } else {
756            Err(RemoteError::UnsupportedUrlScheme)
757        }
758    }
759
760    /// Create a new remote with the given name and URL
761    /// Validates input and handles constraint violations gracefully
762    pub fn create(
763        conn: &OperationsConnection,
764        name: &str,
765        url: &str,
766    ) -> Result<Remote, RemoteError> {
767        // Validate input
768        Self::validate_name(name)?;
769        Self::validate_url(url)?;
770
771        let query = "INSERT INTO remotes (name, url) VALUES (?1, ?2)";
772        let mut stmt = conn.prepare(query)?;
773
774        match stmt.execute(params![name, url]) {
775            Ok(_) => Ok(Remote {
776                name: name.to_string(),
777                url: url.to_string(),
778            }),
779            Err(rusqlite::Error::SqliteFailure(err, _))
780                if err.code == rusqlite::ErrorCode::ConstraintViolation =>
781            {
782                Err(RemoteError::RemoteAlreadyExists(name.to_string()))
783            }
784            Err(e) => Err(RemoteError::DatabaseError(e)),
785        }
786    }
787
788    /// Get a remote by name
789    pub fn get_by_name(conn: &OperationsConnection, name: &str) -> Result<Remote, RemoteError> {
790        let query = "SELECT name, url FROM remotes WHERE name = ?1";
791        match Remote::get(conn, query, params![name]) {
792            Ok(remote) => Ok(remote),
793            Err(rusqlite::Error::QueryReturnedNoRows) => {
794                Err(RemoteError::RemoteNotFound(name.to_string()))
795            }
796            Err(e) => Err(RemoteError::DatabaseError(e)),
797        }
798    }
799
800    /// Get a remote by name, returning None if not found (for backward compatibility)
801    pub fn get_by_name_optional(conn: &OperationsConnection, name: &str) -> Option<Remote> {
802        Self::get_by_name(conn, name).ok()
803    }
804
805    /// List all remotes
806    pub fn list_all(conn: &OperationsConnection) -> Vec<Remote> {
807        Remote::query(
808            conn,
809            "SELECT name, url FROM remotes ORDER BY name",
810            params![],
811        )
812    }
813
814    /// Delete a remote by name
815    pub fn delete(conn: &OperationsConnection, name: &str) -> Result<(), RemoteError> {
816        // Check if remote exists first
817        Self::get_by_name(conn, name)?;
818
819        let query = "DELETE FROM remotes WHERE name = ?1";
820        let mut stmt = conn.prepare(query)?;
821        stmt.execute(params![name])?;
822        Ok(())
823    }
824
825    /// Check if a remote exists
826    pub fn exists(conn: &OperationsConnection, name: &str) -> bool {
827        Self::get_by_name_optional(conn, name).is_some()
828    }
829}
830
831#[derive(Clone, Debug)]
832pub struct Branch {
833    pub id: i64,
834    pub name: String,
835    pub current_operation_hash: Option<HashId>,
836    pub remote_name: Option<String>,
837}
838
839impl Query for Branch {
840    type Model = Branch;
841
842    const TABLE_NAME: &'static str = "branches";
843
844    fn process_row(row: &Row) -> Self::Model {
845        Branch {
846            id: row.get(0).unwrap(),
847            name: row.get(1).unwrap(),
848            current_operation_hash: row.get(2).unwrap(),
849            remote_name: row.get(3).unwrap(),
850        }
851    }
852}
853
854impl Branch {
855    pub fn get_or_create(conn: &OperationsConnection, branch_name: &str) -> Branch {
856        match Branch::create_with_remote(conn, branch_name, None) {
857            Ok(res) => res,
858            Err(rusqlite::Error::SqliteFailure(err, details)) => {
859                if err.code == rusqlite::ErrorCode::ConstraintViolation {
860                    Branch::get_by_name(conn, branch_name)
861                        .unwrap_or_else(|| panic!("No branch named {branch_name}."))
862                } else {
863                    panic!("something bad happened querying the database {err:?} {details:?}");
864                }
865            }
866            Err(_) => {
867                panic!("something bad happened querying the database");
868            }
869        }
870    }
871
872    pub fn create_with_remote(
873        conn: &OperationsConnection,
874        branch_name: &str,
875        remote_name: Option<&str>,
876    ) -> SQLResult<Branch> {
877        let current_operation_hash = OperationState::get_operation(conn);
878        let mut stmt = conn.prepare_cached("insert into branch (name, current_operation_hash, remote_name) values (?1, ?2, ?3) returning (id);").unwrap();
879
880        let mut rows = stmt
881            .query_map((branch_name, current_operation_hash, remote_name), |row| {
882                Ok(Branch {
883                    id: row.get(0)?,
884                    name: branch_name.to_string(),
885                    current_operation_hash,
886                    remote_name: remote_name.map(|s| s.to_string()),
887                })
888            })
889            .unwrap();
890        rows.next().unwrap()
891    }
892
893    pub fn delete(conn: &OperationsConnection, branch_id: i64) -> Result<(), BranchError> {
894        if let Some(current_branch) = OperationState::get_current_branch(conn)
895            && current_branch == branch_id
896        {
897            return Err(BranchError::CannotDelete(
898                "Unable to delete the branch that is currently active.".to_string(),
899            ));
900        }
901        conn.execute("delete from branch where id = ?1", (branch_id,))?;
902        Ok(())
903    }
904
905    pub fn all(conn: &OperationsConnection) -> Vec<Branch> {
906        Branch::query(conn, "select * from branch;", params![])
907    }
908
909    pub fn get_by_name(conn: &OperationsConnection, branch_name: &str) -> Option<Branch> {
910        let mut branch: Option<Branch> = None;
911        let results = Branch::query(
912            conn,
913            "select * from branch where name = ?1",
914            params![branch_name],
915        );
916        for result in results.iter() {
917            branch = Some(result.clone());
918        }
919        branch
920    }
921
922    pub fn get_by_id(conn: &OperationsConnection, branch_id: i64) -> Option<Branch> {
923        let mut branch: Option<Branch> = None;
924        for result in Branch::query(
925            conn,
926            "select * from branch where id = ?1",
927            params![Value::from(branch_id)],
928        )
929        .iter()
930        {
931            branch = Some(result.clone());
932        }
933        branch
934    }
935
936    pub fn set_current_operation(
937        conn: &OperationsConnection,
938        branch_id: i64,
939        operation_hash: &HashId,
940    ) {
941        conn.execute(
942            "UPDATE branch set current_operation_hash = ?2 where id = ?1",
943            params![branch_id, operation_hash],
944        )
945        .unwrap();
946    }
947
948    pub fn get_operations(conn: &OperationsConnection, branch_id: i64) -> Vec<Operation> {
949        let branch = Branch::get_by_id(conn, branch_id)
950            .unwrap_or_else(|| panic!("No branch with id {branch_id}."));
951        if let Some(hash) = branch.current_operation_hash {
952            let hashes = Operation::get_upstream(conn, &hash);
953            hashes
954                .iter()
955                .map(|hash| Operation::get_by_id(conn, hash).unwrap())
956                .collect::<Vec<Operation>>()
957        } else {
958            vec![]
959        }
960    }
961
962    /// Associate a branch with a remote
963    pub fn set_remote(
964        conn: &OperationsConnection,
965        branch_id: i64,
966        remote_name: Option<&str>,
967    ) -> SQLResult<()> {
968        let query = "UPDATE branch SET remote_name = ?1 WHERE id = ?2";
969        let mut stmt = conn.prepare(query)?;
970        stmt.execute(params![remote_name, branch_id])?;
971        Ok(())
972    }
973
974    /// Associate a branch with a remote with validation
975    pub fn set_remote_validated(
976        conn: &OperationsConnection,
977        branch_id: i64,
978        remote_name: Option<&str>,
979    ) -> Result<(), RemoteError> {
980        // If setting a remote name, validate that it exists
981        if let Some(name) = remote_name {
982            Remote::get_by_name(conn, name)?;
983        }
984
985        let query = "UPDATE branch SET remote_name = ?1 WHERE id = ?2";
986        let mut stmt = conn.prepare(query)?;
987        stmt.execute(params![remote_name, branch_id])?;
988        Ok(())
989    }
990
991    /// Get the remote associated with a branch
992    pub fn get_remote(conn: &OperationsConnection, branch_id: i64) -> Option<String> {
993        let query = "SELECT remote_name FROM branch WHERE id = ?1";
994        let mut stmt = conn.prepare(query).ok()?;
995        let mut rows = stmt
996            .query_map(params![branch_id], |row| row.get::<_, Option<String>>(0))
997            .ok()?;
998
999        if let Some(Ok(remote_name)) = rows.next() {
1000            remote_name
1001        } else {
1002            None
1003        }
1004    }
1005}
1006
1007#[derive(Clone, Debug, Serialize, Deserialize)]
1008pub struct Defaults {
1009    pub id: i64,
1010    pub db_name: Option<String>,
1011    pub collection_name: Option<String>,
1012    pub remote_name: Option<String>,
1013}
1014
1015impl Query for Defaults {
1016    type Model = Defaults;
1017
1018    const TABLE_NAME: &'static str = "defaults";
1019
1020    fn process_row(row: &Row) -> Self::Model {
1021        Defaults {
1022            id: row.get(0).unwrap(),
1023            db_name: row.get(1).unwrap(),
1024            collection_name: row.get(2).unwrap(),
1025            remote_name: row.get(3).unwrap(),
1026        }
1027    }
1028}
1029
1030impl Defaults {
1031    /// Set the default remote by name
1032    pub fn set_default_remote(
1033        conn: &OperationsConnection,
1034        remote_name: Option<&str>,
1035    ) -> Result<(), RemoteError> {
1036        // If setting a remote name, validate that it exists
1037        if let Some(name) = remote_name {
1038            Remote::get_by_name(conn, name)?;
1039        }
1040
1041        let query = "UPDATE defaults SET remote_name = ?1 WHERE id = 1";
1042        let mut stmt = conn.prepare(query)?;
1043        stmt.execute(params![remote_name])?;
1044        Ok(())
1045    }
1046
1047    pub fn set_default_remote_compat(
1048        conn: &OperationsConnection,
1049        remote_name: Option<&str>,
1050    ) -> SQLResult<()> {
1051        let query = "UPDATE defaults SET remote_name = ?1 WHERE id = 1";
1052        let mut stmt = conn.prepare(query)?;
1053        stmt.execute(params![remote_name])?;
1054        Ok(())
1055    }
1056
1057    /// Get the default remote name
1058    pub fn get_default_remote(conn: &OperationsConnection) -> Option<String> {
1059        let query = "SELECT remote_name FROM defaults WHERE id = 1";
1060        let mut stmt = conn.prepare(query).ok()?;
1061        let mut rows = stmt
1062            .query_map(params![], |row| row.get::<_, Option<String>>(0))
1063            .ok()?;
1064
1065        if let Some(Ok(remote_name)) = rows.next() {
1066            remote_name
1067        } else {
1068            None
1069        }
1070    }
1071
1072    /// Helper method to get the default remote URL by resolving the remote name
1073    pub fn get_default_remote_url(conn: &OperationsConnection) -> Option<String> {
1074        if let Some(remote_name) = Self::get_default_remote(conn) {
1075            if let Some(remote) = Remote::get_by_name_optional(conn, &remote_name) {
1076                Some(remote.url)
1077            } else {
1078                None
1079            }
1080        } else {
1081            None
1082        }
1083    }
1084
1085    /// Get the defaults record
1086    pub fn get(conn: &OperationsConnection) -> Option<Defaults> {
1087        let query = "SELECT id, db_name, collection_name, remote_name FROM defaults WHERE id = 1";
1088        Self::get_single(conn, query, params![]).ok()
1089    }
1090
1091    /// Helper method to get a single defaults record using the Query trait
1092    fn get_single(
1093        conn: &OperationsConnection,
1094        query: &str,
1095        params: &[&dyn rusqlite::ToSql],
1096    ) -> SQLResult<Defaults> {
1097        let mut stmt = conn.prepare(query)?;
1098        let mut rows = stmt.query_map(params, |row| Ok(Self::process_row(row)))?;
1099
1100        if let Some(row) = rows.next() {
1101            row
1102        } else {
1103            Err(rusqlite::Error::QueryReturnedNoRows)
1104        }
1105    }
1106}
1107
1108pub struct OperationState {}
1109
1110impl OperationState {
1111    pub fn set_operation(conn: &OperationsConnection, op_hash: &HashId) {
1112        let mut stmt = conn
1113            .prepare(
1114                "INSERT INTO operation_state (id, operation_hash)
1115          VALUES (1, ?1)
1116          ON CONFLICT (id) DO
1117          UPDATE SET operation_hash=excluded.operation_hash;",
1118            )
1119            .unwrap();
1120        stmt.execute([op_hash]).unwrap();
1121        let branch_id = OperationState::get_current_branch(conn).expect("No current branch set.");
1122        Branch::set_current_operation(conn, branch_id, op_hash);
1123    }
1124
1125    pub fn get_operation(conn: &OperationsConnection) -> Option<HashId> {
1126        let mut hash: Option<HashId> = None;
1127        let mut stmt = conn
1128            .prepare("SELECT operation_hash from operation_state where id = 1;")
1129            .unwrap();
1130        let rows = stmt.query_map((), |row| row.get(0)).unwrap();
1131        for row in rows {
1132            hash = row.unwrap();
1133        }
1134        hash
1135    }
1136
1137    pub fn set_branch(conn: &OperationsConnection, branch_name: &str) {
1138        let branch = Branch::get_by_name(conn, branch_name)
1139            .unwrap_or_else(|| panic!("No branch named {branch_name}."));
1140        let mut stmt = conn
1141            .prepare(
1142                "INSERT INTO operation_state (id, branch_id)
1143          VALUES (1, ?1)
1144          ON CONFLICT (id) DO
1145          UPDATE SET branch_id=excluded.branch_id;",
1146            )
1147            .unwrap();
1148        stmt.execute(params![branch.id]).unwrap();
1149        if let Some(current_branch_id) = OperationState::get_current_branch(conn) {
1150            if current_branch_id != branch.id {
1151                panic!("Failed to set branch to {branch_name}");
1152            }
1153        } else {
1154            panic!("Failed to set branch.");
1155        }
1156    }
1157
1158    pub fn get_current_branch(conn: &OperationsConnection) -> Option<i64> {
1159        let mut id: Option<i64> = None;
1160        let mut stmt = conn
1161            .prepare("SELECT branch_id from operation_state where id = 1;")
1162            .unwrap();
1163        let rows = stmt.query_map((), |row| row.get(0)).unwrap();
1164        for row in rows {
1165            id = row.unwrap();
1166        }
1167        id
1168    }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173    use std::{
1174        collections::HashSet,
1175        fs,
1176        io::{Cursor, Write},
1177        path::PathBuf,
1178    };
1179
1180    use tempfile::NamedTempFile;
1181
1182    use super::*;
1183    use crate::{
1184        files::GenDatabase,
1185        test_helpers::{create_operation, setup_gen},
1186    };
1187
1188    #[cfg(test)]
1189    mod defaults {
1190        use super::*;
1191
1192        #[test]
1193        fn test_writes_operation_hash() {
1194            let context = setup_gen();
1195            let op_conn = context.operations().conn();
1196
1197            let operation =
1198                Operation::create(op_conn, "test", &HashId::convert_str("some-hash")).unwrap();
1199            OperationState::set_operation(op_conn, &operation.hash);
1200            assert_eq!(
1201                OperationState::get_operation(op_conn).unwrap(),
1202                operation.hash
1203            );
1204        }
1205
1206        #[test]
1207        fn test_default_remote_functionality() {
1208            let context = setup_gen();
1209            let op_conn = context.operations().conn();
1210
1211            // Create test remotes
1212            Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
1213            Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
1214
1215            // Test getting default remote when none is set
1216            assert_eq!(Defaults::get_default_remote(op_conn), None);
1217            assert_eq!(Defaults::get_default_remote_url(op_conn), None);
1218
1219            // Test setting default remote
1220            Defaults::set_default_remote(op_conn, Some("origin")).unwrap();
1221            assert_eq!(
1222                Defaults::get_default_remote(op_conn),
1223                Some("origin".to_string())
1224            );
1225            assert_eq!(
1226                Defaults::get_default_remote_url(op_conn),
1227                Some("https://example.com/repo.gen".to_string())
1228            );
1229
1230            // Test changing default remote
1231            Defaults::set_default_remote(op_conn, Some("upstream")).unwrap();
1232            assert_eq!(
1233                Defaults::get_default_remote(op_conn),
1234                Some("upstream".to_string())
1235            );
1236            assert_eq!(
1237                Defaults::get_default_remote_url(op_conn),
1238                Some("https://upstream.com/repo.gen".to_string())
1239            );
1240
1241            // Test clearing default remote
1242            Defaults::set_default_remote(op_conn, None).unwrap();
1243            assert_eq!(Defaults::get_default_remote(op_conn), None);
1244            assert_eq!(Defaults::get_default_remote_url(op_conn), None);
1245
1246            // Test getting URL for non-existent remote (using the compat method to bypass validation)
1247            Defaults::set_default_remote_compat(op_conn, Some("nonexistent")).unwrap();
1248            assert_eq!(
1249                Defaults::get_default_remote(op_conn),
1250                Some("nonexistent".to_string())
1251            );
1252            assert_eq!(Defaults::get_default_remote_url(op_conn), None);
1253        }
1254
1255        #[test]
1256        fn test_defaults_get() {
1257            let context = setup_gen();
1258            let op_conn = context.operations().conn();
1259
1260            // Test getting defaults record
1261            let defaults = Defaults::get(op_conn).unwrap();
1262            assert_eq!(defaults.id, 1);
1263            assert_eq!(defaults.db_name, None);
1264            assert_eq!(defaults.collection_name, None);
1265            assert_eq!(defaults.remote_name, None);
1266
1267            // Set a default remote and test again (using compat method to bypass validation)
1268            Defaults::set_default_remote_compat(op_conn, Some("test-remote")).unwrap();
1269            let defaults = Defaults::get(op_conn).unwrap();
1270            assert_eq!(defaults.remote_name, Some("test-remote".to_string()));
1271        }
1272    }
1273
1274    #[cfg(test)]
1275    mod remote {
1276        use super::*;
1277
1278        #[test]
1279        fn test_validate_remote_name() {
1280            // Valid names
1281            assert!(Remote::validate_name("origin").is_ok());
1282            assert!(Remote::validate_name("my-remote").is_ok());
1283            assert!(Remote::validate_name("remote_1").is_ok());
1284            assert!(Remote::validate_name("test123").is_ok());
1285
1286            // Invalid names
1287            assert!(Remote::validate_name("").is_err());
1288            assert!(Remote::validate_name("remote with spaces").is_err());
1289            assert!(Remote::validate_name("remote@special").is_err());
1290            assert!(Remote::validate_name("remote.dot").is_err());
1291        }
1292
1293        #[test]
1294        fn test_validate_url() {
1295            // Valid URLs
1296            assert!(Remote::validate_url("https://genhub.bio/user/repo.gen").is_ok());
1297            assert!(Remote::validate_url("http://example.com/repo").is_ok());
1298            assert!(Remote::validate_url("ssh://git@genhub.bio/user/repo.gen").is_ok());
1299            assert!(Remote::validate_url("/path/to/local/repo").is_ok());
1300            assert!(Remote::validate_url("user@host:path/to/repo").is_ok());
1301
1302            // Invalid URLs
1303            assert!(Remote::validate_url("").is_err());
1304            assert!(Remote::validate_url("not-a-url").is_err());
1305
1306            assert!(Remote::validate_url("ftp://invalid-protocol.com").is_err());
1307        }
1308    }
1309
1310    mod branch {
1311        use super::*;
1312
1313        #[test]
1314        fn test_branch_set_remote_valid() {
1315            let context = setup_gen();
1316            let op_conn = context.operations().conn();
1317
1318            // Get database UUID and setup database
1319
1320            // Create test remotes
1321            Remote::create(op_conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
1322
1323            // Create test branch
1324            let branch = Branch::get_or_create(op_conn, "test_branch");
1325
1326            // Initially, branch should have no remote
1327            assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1328
1329            // Set remote
1330            let result = Branch::set_remote(op_conn, branch.id, Some("origin"));
1331            assert!(result.is_ok());
1332
1333            // Verify remote was set
1334            assert_eq!(
1335                Branch::get_remote(op_conn, branch.id),
1336                Some("origin".to_string())
1337            );
1338        }
1339
1340        #[test]
1341        fn test_branch_set_remote_nonexistent() {
1342            let context = setup_gen();
1343            let op_conn = context.operations().conn();
1344
1345            // Get database UUID and setup database
1346
1347            // Create test branch
1348            let branch = Branch::get_or_create(op_conn, "test_branch");
1349
1350            // Try to set a remote that doesn't exist - should fail due to foreign key constraint
1351            let result = Branch::set_remote(op_conn, branch.id, Some("nonexistent"));
1352            assert!(result.is_err());
1353
1354            // Verify branch still has no remote
1355            assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1356        }
1357
1358        #[test]
1359        fn test_branch_clear_remote() {
1360            let context = setup_gen();
1361            let op_conn = context.operations().conn();
1362
1363            // Get database UUID and setup database
1364
1365            // Create test remotes
1366            Remote::create(op_conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
1367
1368            // Create test branch
1369            let branch = Branch::get_or_create(op_conn, "test_branch");
1370
1371            // Set a remote first
1372            Branch::set_remote(op_conn, branch.id, Some("origin")).unwrap();
1373            assert_eq!(
1374                Branch::get_remote(op_conn, branch.id),
1375                Some("origin".to_string())
1376            );
1377
1378            // Clear the remote
1379            Branch::set_remote(op_conn, branch.id, None).unwrap();
1380            assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1381        }
1382
1383        #[test]
1384        fn test_branch_remote_cascade_on_remote_delete() {
1385            let context = setup_gen();
1386            let op_conn = context.operations().conn();
1387
1388            // Get database UUID and setup database
1389
1390            // Create test remotes
1391            Remote::create(op_conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
1392
1393            // Create a branch and associate it with a remote
1394            let branch = Branch::get_or_create(op_conn, "test_branch_cascade");
1395            Branch::set_remote(op_conn, branch.id, Some("origin")).unwrap();
1396
1397            // Verify the association
1398            assert_eq!(
1399                Branch::get_remote(op_conn, branch.id),
1400                Some("origin".to_string())
1401            );
1402
1403            // Delete the remote
1404            Remote::delete(op_conn, "origin").unwrap();
1405
1406            // Verify the branch remote association was set to null
1407            assert_eq!(Branch::get_remote(op_conn, branch.id), None);
1408
1409            // Verify the branch still exists
1410            let branch_from_db = Branch::get_by_id(op_conn, branch.id);
1411            assert!(branch_from_db.is_some());
1412            assert_eq!(branch_from_db.unwrap().remote_name, None);
1413        }
1414    }
1415
1416    mod parse_hash {
1417        use super::*;
1418
1419        #[test]
1420        fn test_parse_hash_head_and_range() {
1421            let context = setup_gen();
1422            let op_conn = context.operations().conn();
1423
1424            let branch = Branch::get_or_create(op_conn, "main");
1425            OperationState::set_branch(op_conn, &branch.name);
1426
1427            let op_1 =
1428                Operation::create(op_conn, "add", &HashId::convert_str("op-1-abc-123")).unwrap();
1429            let op_2 =
1430                Operation::create(op_conn, "add", &HashId::convert_str("op-2-abc-123")).unwrap();
1431
1432            let head = parse_hash(op_conn, "HEAD").unwrap();
1433            assert_eq!(
1434                head,
1435                HashRange {
1436                    from: None,
1437                    to: Some(op_2.hash),
1438                }
1439            );
1440
1441            let range = parse_hash(op_conn, "HEAD~1..HEAD").unwrap();
1442            assert_eq!(
1443                range,
1444                HashRange {
1445                    from: Some(op_1.hash),
1446                    to: Some(op_2.hash),
1447                }
1448            );
1449        }
1450
1451        #[test]
1452        fn test_parse_hash_branch_and_partial() {
1453            let context = setup_gen();
1454            let op_conn = context.operations().conn();
1455
1456            let branch = Branch::get_or_create(op_conn, "main");
1457            OperationState::set_branch(op_conn, &branch.name);
1458
1459            let op_1 =
1460                Operation::create(op_conn, "add", &HashId::convert_str("op-1-xyz-123")).unwrap();
1461            let op_2 =
1462                Operation::create(op_conn, "add", &HashId::convert_str("op-2-xyz-123")).unwrap();
1463
1464            let branch_ref = parse_hash(op_conn, "main").unwrap();
1465            assert_eq!(
1466                branch_ref,
1467                HashRange {
1468                    from: None,
1469                    to: Some(op_2.hash),
1470                }
1471            );
1472
1473            let partial = format!("{}", op_1.hash);
1474            let prefix = &partial[..6];
1475            let resolved = parse_hash(op_conn, prefix).unwrap();
1476            assert_eq!(
1477                resolved,
1478                HashRange {
1479                    from: None,
1480                    to: Some(op_1.hash),
1481                }
1482            );
1483        }
1484
1485        #[test]
1486        fn test_parse_hash_head_offset_out_of_range() {
1487            let context = setup_gen();
1488            let op_conn = context.operations().conn();
1489
1490            let branch = Branch::get_or_create(op_conn, "main");
1491            OperationState::set_branch(op_conn, &branch.name);
1492
1493            let _op = Operation::create(op_conn, "add", &HashId::convert_str("op-1")).unwrap();
1494            let result = parse_hash(op_conn, "HEAD~1");
1495            assert!(matches!(
1496                result,
1497                Err(HashParseError::HeadOffsetOutOfRange(1))
1498            ));
1499        }
1500    }
1501
1502    mod search_hash {
1503        use super::*;
1504
1505        #[test]
1506        fn test_search_hashes_returns_matches() {
1507            let context = setup_gen();
1508            let op_conn = context.operations().conn();
1509
1510            let branch = Branch::get_or_create(op_conn, "main");
1511            OperationState::set_branch(op_conn, &branch.name);
1512
1513            let _op_1 = Operation::create(
1514                op_conn,
1515                "add",
1516                &HashId::pad_str(
1517                    "abc0000000000000000000000000000000000000000000000000000000000001",
1518                ),
1519            )
1520            .unwrap();
1521            let _op_2 = Operation::create(
1522                op_conn,
1523                "add",
1524                &HashId::pad_str(
1525                    "abc0000000000000000000000000000000000000000000000000000000000002",
1526                ),
1527            )
1528            .unwrap();
1529            let _op_3 = Operation::create(
1530                op_conn,
1531                "add",
1532                &HashId::pad_str(
1533                    "def0000000000000000000000000000000000000000000000000000000000003",
1534                ),
1535            )
1536            .unwrap();
1537
1538            let matches = Operation::search_hashes(op_conn, "abc");
1539            assert_eq!(matches.len(), 2);
1540        }
1541
1542        #[test]
1543        fn test_search_hash_resolves_and_errors() {
1544            let context = setup_gen();
1545            let op_conn = context.operations().conn();
1546
1547            let branch = Branch::get_or_create(op_conn, "main");
1548            OperationState::set_branch(op_conn, &branch.name);
1549
1550            let op_unique = Operation::create(
1551                op_conn,
1552                "add",
1553                &HashId::pad_str(
1554                    "def0000000000000000000000000000000000000000000000000000000000001",
1555                ),
1556            )
1557            .unwrap();
1558            let _op_ambiguous = Operation::create(
1559                op_conn,
1560                "add",
1561                &HashId::pad_str(
1562                    "abc0000000000000000000000000000000000000000000000000000000000001",
1563                ),
1564            )
1565            .unwrap();
1566            let _op_ambiguous_2 = Operation::create(
1567                op_conn,
1568                "add",
1569                &HashId::pad_str(
1570                    "abc0000000000000000000000000000000000000000000000000000000000002",
1571                ),
1572            )
1573            .unwrap();
1574
1575            let resolved = Operation::search_hash(op_conn, "def").unwrap();
1576            assert_eq!(resolved.hash, op_unique.hash);
1577
1578            let ambiguous = Operation::search_hash(op_conn, "abc");
1579            assert!(matches!(
1580                ambiguous,
1581                Err(HashParseError::OperationAmbiguous(_))
1582            ));
1583        }
1584    }
1585
1586    mod resolve_reference {
1587        use super::*;
1588
1589        #[test]
1590        fn test_resolve_reference_branch_and_hash() {
1591            let context = setup_gen();
1592            let op_conn = context.operations().conn();
1593
1594            let branch = Branch::get_or_create(op_conn, "main");
1595            OperationState::set_branch(op_conn, &branch.name);
1596
1597            let op_unique = Operation::create(
1598                op_conn,
1599                "add",
1600                &HashId::pad_str(
1601                    "def0000000000000000000000000000000000000000000000000000000000001",
1602                ),
1603            )
1604            .unwrap();
1605
1606            let branch_hash = resolve_reference(op_conn, "main").unwrap();
1607            assert_eq!(branch_hash, op_unique.hash);
1608
1609            let hash_ref = resolve_reference(op_conn, "def").unwrap();
1610            assert_eq!(hash_ref, op_unique.hash);
1611        }
1612
1613        #[test]
1614        fn test_resolve_reference_ambiguous() {
1615            let context = setup_gen();
1616            let op_conn = context.operations().conn();
1617
1618            let branch = Branch::get_or_create(op_conn, "main");
1619            OperationState::set_branch(op_conn, &branch.name);
1620
1621            let _op_1 = Operation::create(
1622                op_conn,
1623                "add",
1624                &HashId::pad_str(
1625                    "abc0000000000000000000000000000000000000000000000000000000000001",
1626                ),
1627            )
1628            .unwrap();
1629            let _op_2 = Operation::create(
1630                op_conn,
1631                "add",
1632                &HashId::pad_str(
1633                    "abc0000000000000000000000000000000000000000000000000000000000002",
1634                ),
1635            )
1636            .unwrap();
1637
1638            let result = resolve_reference(op_conn, "abc");
1639            assert!(matches!(result, Err(HashParseError::OperationAmbiguous(_))));
1640        }
1641    }
1642
1643    mod resolve_head {
1644        use super::*;
1645
1646        #[test]
1647        fn test_resolve_head_variants() {
1648            let context = setup_gen();
1649            let op_conn = context.operations().conn();
1650
1651            let branch = Branch::get_or_create(op_conn, "main");
1652            OperationState::set_branch(op_conn, &branch.name);
1653
1654            let op_1 = Operation::create(op_conn, "add", &HashId::convert_str("op-1")).unwrap();
1655            let op_2 = Operation::create(op_conn, "add", &HashId::convert_str("op-2")).unwrap();
1656
1657            let head = resolve_head(op_conn, "HEAD").unwrap();
1658            assert_eq!(head, op_2.hash);
1659
1660            let head_prev = resolve_head(op_conn, "HEAD~1").unwrap();
1661            assert_eq!(head_prev, op_1.hash);
1662        }
1663
1664        #[test]
1665        fn test_resolve_head_invalid() {
1666            let context = setup_gen();
1667            let op_conn = context.operations().conn();
1668
1669            let branch = Branch::get_or_create(op_conn, "main");
1670            OperationState::set_branch(op_conn, &branch.name);
1671
1672            let _op = Operation::create(op_conn, "add", &HashId::convert_str("op-1")).unwrap();
1673            let result = resolve_head(op_conn, "HEAD~2");
1674            assert!(matches!(
1675                result,
1676                Err(HashParseError::HeadOffsetOutOfRange(2))
1677            ));
1678        }
1679    }
1680
1681    #[test]
1682    fn test_create_operation_adds_database() {
1683        let context = setup_gen();
1684        let conn = context.graph().conn();
1685        let op_conn = context.operations().conn();
1686        let db_uuid = crate::metadata::get_db_uuid(conn);
1687        let gen_db = GenDatabase::create(op_conn, &db_uuid, "foo.db", "/foo.db").unwrap();
1688
1689        let op = create_operation(
1690            &context,
1691            "something.fa",
1692            FileTypes::Fasta,
1693            "foo",
1694            HashId::convert_str("op-1"),
1695        );
1696
1697        let databases = GenDatabase::query_by_operations(op_conn, &[op.hash]).unwrap();
1698        assert_eq!(databases[&op.hash], vec![gen_db]);
1699    }
1700
1701    #[test]
1702    fn test_gets_operations_of_branch() {
1703        let context = setup_gen();
1704        let conn = context.graph().conn();
1705        let op_conn = context.operations().conn();
1706
1707        let db_uuid = crate::metadata::get_db_uuid(conn);
1708        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
1709
1710        create_operation(
1711            &context,
1712            "test.fasta",
1713            FileTypes::Fasta,
1714            "foo",
1715            HashId::convert_str("op-1"),
1716        );
1717        // operations will be made in ascending order.
1718        // The branch topology is as follows. () indicate where a branch starts
1719        //
1720        //                     -> 4 -> 5
1721        //                   /
1722        //         -> 2 -> 3 (branch-1-sub-1)
1723        //        /
1724        //      branch-1
1725        //    /
1726        //   1 (main, branch-1, branch-2)
1727        //    \
1728        //    branch-2
1729        //       \
1730        //        -> 6 -> 7 (branch-2-midpoint-1) -> 8 (branch-2-sub-1)
1731        //                 \                           \
1732        //                   -> 12 -> 13                9 -> 10 -> 11
1733        //
1734        //
1735        //
1736        //
1737        create_operation(
1738            &context,
1739            "test.fasta",
1740            FileTypes::Fasta,
1741            "foo",
1742            HashId::convert_str("op-2"),
1743        );
1744        create_operation(
1745            &context,
1746            "test.fasta",
1747            FileTypes::Fasta,
1748            "foo",
1749            HashId::convert_str("op-3"),
1750        );
1751        create_operation(
1752            &context,
1753            "test.fasta",
1754            FileTypes::Fasta,
1755            "foo",
1756            HashId::convert_str("op-4"),
1757        );
1758        create_operation(
1759            &context,
1760            "test.fasta",
1761            FileTypes::Fasta,
1762            "foo",
1763            HashId::convert_str("op-5"),
1764        );
1765        OperationState::set_operation(op_conn, &HashId::convert_str("op-1"));
1766        create_operation(
1767            &context,
1768            "test.fasta",
1769            FileTypes::Fasta,
1770            "foo",
1771            HashId::convert_str("op-6"),
1772        );
1773        let _branch_2_midpoint = create_operation(
1774            &context,
1775            "test.fasta",
1776            FileTypes::Fasta,
1777            "foo",
1778            HashId::convert_str("op-7"),
1779        );
1780        create_operation(
1781            &context,
1782            "test.fasta",
1783            FileTypes::Fasta,
1784            "foo",
1785            HashId::convert_str("op-8"),
1786        );
1787        create_operation(
1788            &context,
1789            "test.fasta",
1790            FileTypes::Fasta,
1791            "foo",
1792            HashId::convert_str("op-9"),
1793        );
1794        create_operation(
1795            &context,
1796            "test.fasta",
1797            FileTypes::Fasta,
1798            "foo",
1799            HashId::convert_str("op-10"),
1800        );
1801        create_operation(
1802            &context,
1803            "test.fasta",
1804            FileTypes::Fasta,
1805            "foo",
1806            HashId::convert_str("op-11"),
1807        );
1808        OperationState::set_operation(op_conn, &HashId::convert_str("op-7"));
1809        create_operation(
1810            &context,
1811            "test.fasta",
1812            FileTypes::Fasta,
1813            "foo",
1814            HashId::convert_str("op-12"),
1815        );
1816        create_operation(
1817            &context,
1818            "test.fasta",
1819            FileTypes::Fasta,
1820            "foo",
1821            HashId::convert_str("op-13"),
1822        );
1823
1824        OperationState::set_operation(op_conn, &HashId::convert_str("op-3"));
1825        let branch_1 = Branch::get_or_create(op_conn, "branch-1");
1826        OperationState::set_operation(op_conn, &HashId::convert_str("op-8"));
1827        let branch_2 = Branch::get_or_create(op_conn, "branch-2");
1828        OperationState::set_operation(op_conn, &HashId::convert_str("op-5"));
1829        let branch_1_sub_1 = Branch::get_or_create(op_conn, "branch-1-sub-1");
1830        OperationState::set_operation(op_conn, &HashId::convert_str("op-11"));
1831        let branch_2_sub_1 = Branch::get_or_create(op_conn, "branch-2-sub-1");
1832        OperationState::set_operation(op_conn, &HashId::convert_str("op-13"));
1833        let branch_2_midpoint_1 = Branch::get_or_create(op_conn, "branch-2-midpoint-1");
1834
1835        let ops = Branch::get_operations(op_conn, branch_2_midpoint_1.id)
1836            .iter()
1837            .map(|f| f.hash)
1838            .collect::<Vec<_>>();
1839        assert_eq!(
1840            ops,
1841            vec![
1842                HashId::convert_str("op-1"),
1843                HashId::convert_str("op-6"),
1844                HashId::convert_str("op-7"),
1845                HashId::convert_str("op-12"),
1846                HashId::convert_str("op-13")
1847            ]
1848        );
1849
1850        let ops = Branch::get_operations(op_conn, branch_1.id)
1851            .iter()
1852            .map(|f| f.hash)
1853            .collect::<Vec<_>>();
1854        assert_eq!(
1855            ops,
1856            vec![
1857                HashId::convert_str("op-1"),
1858                HashId::convert_str("op-2"),
1859                HashId::convert_str("op-3")
1860            ]
1861        );
1862
1863        let ops = Branch::get_operations(op_conn, branch_2.id)
1864            .iter()
1865            .map(|f| f.hash)
1866            .collect::<Vec<_>>();
1867        assert_eq!(
1868            ops,
1869            vec![
1870                HashId::convert_str("op-1"),
1871                HashId::convert_str("op-6"),
1872                HashId::convert_str("op-7"),
1873                HashId::convert_str("op-8")
1874            ]
1875        );
1876
1877        let ops = Branch::get_operations(op_conn, branch_1_sub_1.id)
1878            .iter()
1879            .map(|f| f.hash)
1880            .collect::<Vec<_>>();
1881        assert_eq!(
1882            ops,
1883            vec![
1884                HashId::convert_str("op-1"),
1885                HashId::convert_str("op-2"),
1886                HashId::convert_str("op-3"),
1887                HashId::convert_str("op-4"),
1888                HashId::convert_str("op-5")
1889            ]
1890        );
1891
1892        let ops = Branch::get_operations(op_conn, branch_2_sub_1.id)
1893            .iter()
1894            .map(|f: &Operation| f.hash)
1895            .collect::<Vec<_>>();
1896        assert_eq!(
1897            ops,
1898            vec![
1899                HashId::convert_str("op-1"),
1900                HashId::convert_str("op-6"),
1901                HashId::convert_str("op-7"),
1902                HashId::convert_str("op-8"),
1903                HashId::convert_str("op-9"),
1904                HashId::convert_str("op-10"),
1905                HashId::convert_str("op-11")
1906            ]
1907        );
1908    }
1909
1910    #[test]
1911    fn test_graph_representation() {
1912        let context = setup_gen();
1913        let op_conn = context.operations().conn();
1914
1915        // operations will be made in ascending order.
1916        // The branch topology is as follows. () indicate where a branch starts
1917        //
1918        //
1919        //
1920        //    branch-3   /-> 7
1921        //    main      1 -> 2 -> 3
1922        //    branch-1             \-> 4 -> 5
1923        //    branch-2                  \-> 6
1924
1925        let mut expected_graph = OperationGraph::new();
1926        expected_graph.add_edge(HashId::convert_str("op-1"), HashId::convert_str("op-2"), ());
1927        expected_graph.add_edge(HashId::convert_str("op-2"), HashId::convert_str("op-3"), ());
1928        expected_graph.add_edge(HashId::convert_str("op-3"), HashId::convert_str("op-4"), ());
1929        expected_graph.add_edge(HashId::convert_str("op-4"), HashId::convert_str("op-5"), ());
1930        expected_graph.add_edge(HashId::convert_str("op-4"), HashId::convert_str("op-6"), ());
1931        expected_graph.add_edge(HashId::convert_str("op-1"), HashId::convert_str("op-7"), ());
1932
1933        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-1")).unwrap();
1934        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-2")).unwrap();
1935        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-3")).unwrap();
1936        Branch::get_or_create(op_conn, "branch-1");
1937        OperationState::set_branch(op_conn, "branch-1");
1938        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-4")).unwrap();
1939        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-5")).unwrap();
1940        OperationState::set_operation(op_conn, &HashId::convert_str("op-4"));
1941        Branch::get_or_create(op_conn, "branch-2");
1942        OperationState::set_branch(op_conn, "branch-2");
1943        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-6")).unwrap();
1944        OperationState::set_operation(op_conn, &HashId::convert_str("op-1"));
1945        Branch::get_or_create(op_conn, "branch-3");
1946        OperationState::set_branch(op_conn, "branch-3");
1947        let _ = Operation::create(op_conn, "vcf_addition", &HashId::convert_str("op-7")).unwrap();
1948        let graph = Operation::get_operation_graph(op_conn);
1949
1950        assert_eq!(
1951            graph.nodes().collect::<HashSet<_>>(),
1952            expected_graph.nodes().collect::<HashSet<_>>()
1953        );
1954        assert_eq!(
1955            graph.all_edges().collect::<HashSet<_>>(),
1956            expected_graph.all_edges().collect::<HashSet<_>>()
1957        );
1958    }
1959
1960    #[test]
1961    fn test_path_between() {
1962        let context = setup_gen();
1963        let conn = context.graph().conn();
1964        let op_conn = context.operations().conn();
1965
1966        let db_uuid = crate::metadata::get_db_uuid(conn);
1967        crate::files::GenDatabase::create(op_conn, &db_uuid, "test_db", "test_db_path").unwrap();
1968
1969        // operations will be made in ascending order.
1970        // The branch topology is as follows. () indicate where a branch starts
1971        //
1972        //
1973        //
1974        //    branch-3   /-> 7
1975        //    main      1 -> 2 -> 3
1976        //    branch-1             \-> 4 -> 5
1977        //    branch-2                  \-> 6
1978
1979        create_operation(
1980            &context,
1981            "test.fasta",
1982            FileTypes::Fasta,
1983            "foo",
1984            HashId::convert_str("op-1"),
1985        );
1986        create_operation(
1987            &context,
1988            "test.fasta",
1989            FileTypes::Fasta,
1990            "foo",
1991            HashId::convert_str("op-2"),
1992        );
1993        create_operation(
1994            &context,
1995            "test.fasta",
1996            FileTypes::Fasta,
1997            "foo",
1998            HashId::convert_str("op-3"),
1999        );
2000        Branch::get_or_create(op_conn, "branch-1");
2001        OperationState::set_branch(op_conn, "branch-1");
2002        create_operation(
2003            &context,
2004            "test.fasta",
2005            FileTypes::Fasta,
2006            "foo",
2007            HashId::convert_str("op-4"),
2008        );
2009        create_operation(
2010            &context,
2011            "test.fasta",
2012            FileTypes::Fasta,
2013            "foo",
2014            HashId::convert_str("op-5"),
2015        );
2016        OperationState::set_operation(op_conn, &HashId::convert_str("op-4"));
2017        Branch::get_or_create(op_conn, "branch-2");
2018        OperationState::set_branch(op_conn, "branch-2");
2019        create_operation(
2020            &context,
2021            "test.fasta",
2022            FileTypes::Fasta,
2023            "foo",
2024            HashId::convert_str("op-6"),
2025        );
2026        OperationState::set_operation(op_conn, &HashId::convert_str("op-1"));
2027        Branch::get_or_create(op_conn, "branch-3");
2028        OperationState::set_branch(op_conn, "branch-3");
2029        create_operation(
2030            &context,
2031            "test.fasta",
2032            FileTypes::Fasta,
2033            "foo",
2034            HashId::convert_str("op-7"),
2035        );
2036        assert_eq!(
2037            Operation::get_path_between(
2038                op_conn,
2039                HashId::convert_str("op-1"),
2040                HashId::convert_str("op-6")
2041            ),
2042            vec![
2043                (
2044                    HashId::convert_str("op-1"),
2045                    Direction::Outgoing,
2046                    HashId::convert_str("op-2")
2047                ),
2048                (
2049                    HashId::convert_str("op-2"),
2050                    Direction::Outgoing,
2051                    HashId::convert_str("op-3")
2052                ),
2053                (
2054                    HashId::convert_str("op-3"),
2055                    Direction::Outgoing,
2056                    HashId::convert_str("op-4")
2057                ),
2058                (
2059                    HashId::convert_str("op-4"),
2060                    Direction::Outgoing,
2061                    HashId::convert_str("op-6")
2062                ),
2063            ]
2064        );
2065
2066        assert_eq!(
2067            Operation::get_path_between(
2068                op_conn,
2069                HashId::convert_str("op-7"),
2070                HashId::convert_str("op-1")
2071            ),
2072            vec![(
2073                HashId::convert_str("op-7"),
2074                Direction::Incoming,
2075                HashId::convert_str("op-1")
2076            ),]
2077        );
2078
2079        assert_eq!(
2080            Operation::get_path_between(
2081                op_conn,
2082                HashId::convert_str("op-3"),
2083                HashId::convert_str("op-7")
2084            ),
2085            vec![
2086                (
2087                    HashId::convert_str("op-3"),
2088                    Direction::Incoming,
2089                    HashId::convert_str("op-2")
2090                ),
2091                (
2092                    HashId::convert_str("op-2"),
2093                    Direction::Incoming,
2094                    HashId::convert_str("op-1")
2095                ),
2096                (
2097                    HashId::convert_str("op-1"),
2098                    Direction::Outgoing,
2099                    HashId::convert_str("op-7")
2100                ),
2101            ]
2102        );
2103    }
2104
2105    #[test]
2106    fn test_remote_create() {
2107        let context = setup_gen();
2108        let op_conn = context.operations().conn();
2109
2110        // Test successful remote creation
2111        let remote = Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2112        assert_eq!(remote.name, "origin");
2113        assert_eq!(remote.url, "https://example.com/repo.gen");
2114
2115        // Test duplicate name constraint violation
2116        let result = Remote::create(op_conn, "origin", "https://different.com/repo.gen");
2117        assert!(result.is_err());
2118    }
2119
2120    #[test]
2121    fn test_remote_get_by_name() {
2122        let context = setup_gen();
2123        let op_conn = context.operations().conn();
2124
2125        // Test getting non-existent remote
2126        let result = Remote::get_by_name_optional(op_conn, "nonexistent");
2127        assert!(result.is_none());
2128
2129        // Create a remote and test retrieval
2130        Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
2131        let result = Remote::get_by_name_optional(op_conn, "upstream");
2132        assert!(result.is_some());
2133        let remote = result.unwrap();
2134        assert_eq!(remote.name, "upstream");
2135        assert_eq!(remote.url, "https://upstream.com/repo.gen");
2136    }
2137
2138    #[test]
2139    fn test_remote_list_all() {
2140        let context = setup_gen();
2141        let op_conn = context.operations().conn();
2142
2143        // Test empty list
2144        let remotes = Remote::list_all(op_conn);
2145        assert!(remotes.is_empty());
2146
2147        // Create multiple remotes
2148        Remote::create(op_conn, "origin", "https://origin.com/repo.gen").unwrap();
2149        Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
2150        Remote::create(op_conn, "fork", "https://fork.com/repo.gen").unwrap();
2151
2152        // Test list returns all remotes in alphabetical order
2153        let remotes = Remote::list_all(op_conn);
2154        assert_eq!(remotes.len(), 3);
2155        assert_eq!(remotes[0].name, "fork");
2156        assert_eq!(remotes[1].name, "origin");
2157        assert_eq!(remotes[2].name, "upstream");
2158    }
2159
2160    #[test]
2161    fn test_remote_delete() {
2162        let context = setup_gen();
2163        let op_conn = context.operations().conn();
2164
2165        // Create a remote
2166        Remote::create(op_conn, "temp", "https://temp.com/repo.gen").unwrap();
2167
2168        // Verify it exists
2169        let remote = Remote::get_by_name_optional(op_conn, "temp");
2170        assert!(remote.is_some());
2171
2172        // Delete the remote
2173        let result = Remote::delete(op_conn, "temp");
2174        assert!(result.is_ok());
2175
2176        // Verify it's gone
2177        let remote = Remote::get_by_name_optional(op_conn, "temp");
2178        assert!(remote.is_none());
2179
2180        // Test deleting non-existent remote (should return error)
2181        let result = Remote::delete(op_conn, "nonexistent");
2182        assert!(result.is_err());
2183    }
2184
2185    #[test]
2186    fn test_remote_delete_with_branch_associations() {
2187        let context = setup_gen();
2188        let op_conn = context.operations().conn();
2189
2190        // Create a remote
2191        Remote::create(op_conn, "test_remote", "https://test.com/repo.gen").unwrap();
2192
2193        // Create a branch and associate it with the remote
2194        let branch = Branch::get_or_create(op_conn, "test_branch");
2195
2196        // Set the remote association (this would be done by the Branch::set_remote method when implemented)
2197        op_conn
2198            .execute(
2199                "UPDATE branch SET remote_name = ?1 WHERE id = ?2",
2200                params!["test_remote", branch.id],
2201            )
2202            .unwrap();
2203
2204        // Verify the association exists
2205        let remote_name: Option<String> = op_conn
2206            .query_row(
2207                "SELECT remote_name FROM branch WHERE id = ?1",
2208                params![branch.id],
2209                |row| row.get(0),
2210            )
2211            .unwrap();
2212        assert_eq!(remote_name, Some("test_remote".to_string()));
2213
2214        // Delete the remote - this should succeed and automatically set branch remote_name to NULL
2215        let result = Remote::delete(op_conn, "test_remote");
2216        assert!(result.is_ok());
2217
2218        // Verify the branch association was automatically cleared by the foreign key constraint
2219        let remote_name_after_delete: Option<String> = op_conn
2220            .query_row(
2221                "SELECT remote_name FROM branch WHERE id = ?1",
2222                params![branch.id],
2223                |row| row.get(0),
2224            )
2225            .unwrap();
2226        assert_eq!(remote_name_after_delete, None);
2227
2228        // Verify the remote was actually deleted
2229        let remote = Remote::get_by_name_optional(op_conn, "test_remote");
2230        assert!(remote.is_none());
2231    }
2232
2233    #[test]
2234    fn test_branch_set_remote() {
2235        let context = setup_gen();
2236        let op_conn = context.operations().conn();
2237
2238        // Create a remote
2239        Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2240
2241        // Create a branch
2242        let branch = Branch::get_or_create(op_conn, "test_branch");
2243
2244        // Initially, branch should have no remote
2245        let remote = Branch::get_remote(op_conn, branch.id);
2246        assert_eq!(remote, None);
2247
2248        // Set the remote association
2249        Branch::set_remote(op_conn, branch.id, Some("origin")).unwrap();
2250
2251        // Verify the association was set
2252        let remote = Branch::get_remote(op_conn, branch.id);
2253        assert_eq!(remote, Some("origin".to_string()));
2254
2255        // Clear the remote association
2256        Branch::set_remote(op_conn, branch.id, None).unwrap();
2257
2258        // Verify the association was cleared
2259        let remote = Branch::get_remote(op_conn, branch.id);
2260        assert_eq!(remote, None);
2261    }
2262
2263    #[test]
2264    fn test_branch_get_remote() {
2265        let context = setup_gen();
2266        let op_conn = context.operations().conn();
2267
2268        // Create remotes
2269        Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2270        Remote::create(op_conn, "upstream", "https://upstream.com/repo.gen").unwrap();
2271
2272        // Create branches
2273        let branch1 = Branch::get_or_create(op_conn, "branch1");
2274        let branch2 = Branch::get_or_create(op_conn, "branch2");
2275
2276        // Set different remotes for each branch
2277        Branch::set_remote(op_conn, branch1.id, Some("origin")).unwrap();
2278        Branch::set_remote(op_conn, branch2.id, Some("upstream")).unwrap();
2279
2280        // Verify each branch has the correct remote
2281        assert_eq!(
2282            Branch::get_remote(op_conn, branch1.id),
2283            Some("origin".to_string())
2284        );
2285        assert_eq!(
2286            Branch::get_remote(op_conn, branch2.id),
2287            Some("upstream".to_string())
2288        );
2289
2290        // Test non-existent branch
2291        assert_eq!(Branch::get_remote(op_conn, 99999), None);
2292    }
2293
2294    #[test]
2295    fn test_branch_create_with_remote() {
2296        let context = setup_gen();
2297        let op_conn = context.operations().conn();
2298
2299        // Create a remote
2300        Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2301
2302        // Create a branch with remote association
2303        let branch = Branch::create_with_remote(op_conn, "test_branch", Some("origin")).unwrap();
2304
2305        // Verify the branch was created with the remote association
2306        assert_eq!(branch.remote_name, Some("origin".to_string()));
2307        assert_eq!(
2308            Branch::get_remote(op_conn, branch.id),
2309            Some("origin".to_string())
2310        );
2311
2312        // Create a branch without remote association
2313        let branch2 = Branch::create_with_remote(op_conn, "test_branch2", None).unwrap();
2314        assert_eq!(branch2.remote_name, None);
2315        assert_eq!(Branch::get_remote(op_conn, branch2.id), None);
2316    }
2317
2318    #[test]
2319    fn test_branch_process_row_with_remote() {
2320        let context = setup_gen();
2321        let op_conn = context.operations().conn();
2322
2323        // Create a remote
2324        Remote::create(op_conn, "origin", "https://example.com/repo.gen").unwrap();
2325
2326        // Create a branch with remote
2327        let branch = Branch::create_with_remote(op_conn, "test_branch", Some("origin")).unwrap();
2328
2329        // Query the branch back to test process_row
2330        let branches = Branch::query(
2331            op_conn,
2332            "SELECT * FROM branch WHERE id = ?1",
2333            params![branch.id],
2334        );
2335        assert_eq!(branches.len(), 1);
2336
2337        let queried_branch = &branches[0];
2338        assert_eq!(queried_branch.id, branch.id);
2339        assert_eq!(queried_branch.name, "test_branch");
2340        assert_eq!(queried_branch.remote_name, Some("origin".to_string()));
2341    }
2342
2343    #[test]
2344    fn test_branch_set_remote_foreign_key_constraint() {
2345        let context = setup_gen();
2346        let op_conn = context.operations().conn();
2347
2348        // Create a branch
2349        let branch = Branch::get_or_create(op_conn, "test_branch");
2350
2351        // Try to set a remote that doesn't exist - this should fail due to foreign key constraint
2352        let result = Branch::set_remote(op_conn, branch.id, Some("nonexistent_remote"));
2353        assert!(result.is_err());
2354
2355        // Verify the branch still has no remote
2356        let remote = Branch::get_remote(op_conn, branch.id);
2357        assert_eq!(remote, None);
2358    }
2359
2360    #[test]
2361    fn operation_capnp_serialization() {
2362        use capnp::message::TypedBuilder;
2363
2364        let model = Operation {
2365            hash: HashId::convert_str("test"),
2366            parent_hash: Some(HashId::convert_str("parent")),
2367            change_type: "foo".to_string(),
2368            created_on: 0,
2369        };
2370
2371        let mut message = TypedBuilder::<operation::Owned>::new_default();
2372        let mut root = message.init_root();
2373        model.write_capnp(&mut root);
2374
2375        let deserialized = Operation::read_capnp(root.into_reader());
2376        assert_eq!(model, deserialized);
2377    }
2378
2379    #[test]
2380    fn operation_capnp_serialization_no_parent() {
2381        use capnp::message::TypedBuilder;
2382
2383        let model = Operation {
2384            hash: HashId::convert_str("test"),
2385            parent_hash: None,
2386            change_type: "foo".to_string(),
2387            created_on: 1,
2388        };
2389
2390        let mut message = TypedBuilder::<operation::Owned>::new_default();
2391        let mut root = message.init_root();
2392        model.write_capnp(&mut root);
2393
2394        let deserialized = Operation::read_capnp(root.into_reader());
2395        assert_eq!(model, deserialized);
2396    }
2397
2398    #[test]
2399    fn file_addition_capnp_serialization() {
2400        use capnp::message::TypedBuilder;
2401
2402        let file_addition = FileAddition {
2403            id: HashId([42u8; 32]),
2404            file_path: "test/path.fasta".to_string(),
2405            file_type: FileTypes::Fasta,
2406            checksum: HashId([24u8; 32]),
2407        };
2408
2409        let mut message =
2410            TypedBuilder::<crate::gen_models_capnp::file_addition::Owned>::new_default();
2411        let mut root = message.init_root();
2412        file_addition.write_capnp(&mut root);
2413
2414        let deserialized = FileAddition::read_capnp(root.into_reader());
2415        assert_eq!(file_addition, deserialized);
2416    }
2417
2418    #[test]
2419    fn operation_summary_capnp_serialization() {
2420        use capnp::message::TypedBuilder;
2421
2422        let operation_summary = OperationSummary {
2423            id: 123,
2424            operation_hash: HashId::convert_str("op-hash-123"),
2425            summary: "Added new sequences from FASTA file".to_string(),
2426        };
2427
2428        let mut message =
2429            TypedBuilder::<crate::gen_models_capnp::operation_summary::Owned>::new_default();
2430        let mut root = message.init_root();
2431        operation_summary.write_capnp(&mut root);
2432
2433        let deserialized = OperationSummary::read_capnp(root.into_reader());
2434        assert_eq!(operation_summary, deserialized);
2435    }
2436
2437    #[test]
2438    fn test_calculate_stream_hash() {
2439        let content = b"Hello, World!";
2440        let cursor = Cursor::new(content);
2441        let hash = calculate_stream_hash(cursor).unwrap();
2442
2443        assert_eq!(hash.len(), 32);
2444
2445        // Test consistency - same content should produce same hash
2446        let cursor2 = Cursor::new(content);
2447        let hash2 = calculate_stream_hash(cursor2).unwrap();
2448        assert_eq!(hash, hash2);
2449
2450        // Test different content produces different hash
2451        let different_content = b"Hello, World!!";
2452        let cursor3 = Cursor::new(different_content);
2453        let hash3 = calculate_stream_hash(cursor3).unwrap();
2454        assert_ne!(hash, hash3);
2455    }
2456
2457    #[test]
2458    fn test_calculate_file_checksum() {
2459        let mut temp_file = NamedTempFile::new().unwrap();
2460        let content = b"Test file content for checksum calculation";
2461        temp_file.write_all(content).unwrap();
2462        temp_file.flush().unwrap();
2463
2464        let checksum = calculate_file_checksum(temp_file.path()).unwrap();
2465
2466        assert_eq!(checksum.0.len(), 32);
2467
2468        // Test consistency - same file should produce same checksum
2469        let checksum2 = calculate_file_checksum(temp_file.path()).unwrap();
2470        assert_eq!(checksum, checksum2);
2471
2472        // Test with different file content
2473        let mut temp_file2 = NamedTempFile::new().unwrap();
2474        let different_content = b"Different test file content";
2475        temp_file2.write_all(different_content).unwrap();
2476        temp_file2.flush().unwrap();
2477
2478        let checksum3 = calculate_file_checksum(temp_file2.path()).unwrap();
2479        assert_ne!(checksum, checksum3);
2480    }
2481
2482    #[test]
2483    fn test_calculate_file_checksum_nonexistent_file() {
2484        let result = calculate_file_checksum("/nonexistent/file/path");
2485        assert!(result.is_err());
2486        assert!(matches!(
2487            result.unwrap_err().kind(),
2488            std::io::ErrorKind::NotFound
2489        ));
2490    }
2491
2492    #[test]
2493    fn test_generate_file_addition_id_consistency() {
2494        let checksum = HashId([1u8; 32]);
2495        let file_path = "/path/to/file.txt";
2496
2497        let id1 = FileAddition::generate_file_addition_id(&checksum, file_path);
2498        let id2 = FileAddition::generate_file_addition_id(&checksum, file_path);
2499
2500        assert_eq!(id1, id2);
2501    }
2502
2503    #[test]
2504    fn test_generate_file_addition_id_uniqueness_different_paths() {
2505        let checksum = HashId([1u8; 32]);
2506        let file_path1 = "/path/to/file1.txt";
2507        let file_path2 = "/path/to/file2.txt";
2508
2509        let id1 = FileAddition::generate_file_addition_id(&checksum, file_path1);
2510        let id2 = FileAddition::generate_file_addition_id(&checksum, file_path2);
2511
2512        assert_ne!(id1, id2);
2513    }
2514
2515    #[test]
2516    fn test_generate_file_addition_id_uniqueness_different_checksums() {
2517        let checksum1 = HashId([1u8; 32]);
2518        let checksum2 = HashId([2u8; 32]);
2519        let file_path = "/path/to/file.txt";
2520
2521        let id1 = FileAddition::generate_file_addition_id(&checksum1, file_path);
2522        let id2 = FileAddition::generate_file_addition_id(&checksum2, file_path);
2523
2524        assert_ne!(id1, id2);
2525    }
2526
2527    #[test]
2528    fn test_normalize_file_paths_absolute_path_in_repo() {
2529        let context = setup_gen();
2530        let workspace = context.workspace();
2531        let repo_root = workspace.base_dir();
2532
2533        let absolute_path = repo_root.join("inputs").join("absolute.txt");
2534        fs::create_dir_all(absolute_path.parent().unwrap()).unwrap();
2535        fs::write(&absolute_path, b"absolute").unwrap();
2536        let absolute_string = absolute_path.to_string_lossy().to_string();
2537        let relative_string = absolute_path
2538            .strip_prefix(repo_root)
2539            .unwrap()
2540            .to_string_lossy()
2541            .to_string();
2542
2543        let (absolute, relative) =
2544            FileAddition::normalize_file_paths(workspace, absolute_string.as_str());
2545
2546        assert_eq!(absolute, absolute_string);
2547        assert_eq!(relative, relative_string);
2548    }
2549
2550    #[test]
2551    fn test_normalize_file_paths_relative_path_in_repo() {
2552        let context = setup_gen();
2553        let workspace = context.workspace();
2554        let repo_root = workspace.repo_root().unwrap();
2555
2556        let relative_path = PathBuf::from("relative/path/file.txt");
2557        let absolute_path = repo_root.join(&relative_path);
2558        fs::create_dir_all(absolute_path.parent().unwrap()).unwrap();
2559        fs::write(&absolute_path, b"relative").unwrap();
2560        let relative_string = relative_path.to_string_lossy().to_string();
2561        let absolute_string = absolute_path.to_string_lossy().to_string();
2562
2563        let (absolute, relative) =
2564            FileAddition::normalize_file_paths(workspace, relative_string.as_str());
2565
2566        assert_eq!(absolute, absolute_string);
2567        assert_eq!(relative, relative_string);
2568    }
2569
2570    #[test]
2571    fn test_normalize_file_paths_outside_repo_fallbacks() {
2572        let context = setup_gen();
2573        let workspace = context.workspace();
2574
2575        let outside_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
2576        let outside_string = outside_path.to_string_lossy().to_string();
2577
2578        let (absolute, relative) =
2579            FileAddition::normalize_file_paths(workspace, outside_string.as_str());
2580
2581        assert_eq!(absolute, outside_string);
2582        assert_eq!(relative, outside_string);
2583    }
2584
2585    #[test]
2586    fn test_normalize_file_paths_without_connection_path() {
2587        let context = setup_gen();
2588        let workspace = context.workspace();
2589
2590        let (absolute, relative) =
2591            FileAddition::normalize_file_paths(workspace, "detached/file.txt");
2592        assert_eq!(absolute, "detached/file.txt");
2593        assert_eq!(relative, "detached/file.txt");
2594
2595        let (absolute_empty, relative_empty) = FileAddition::normalize_file_paths(workspace, "");
2596        assert_eq!(absolute_empty, "");
2597        assert_eq!(relative_empty, "");
2598    }
2599
2600    #[test]
2601    fn test_file_addition_get_or_create() {
2602        let context = setup_gen();
2603        let op_conn = context.operations().conn();
2604        let repo_root = context.workspace().repo_root().unwrap();
2605
2606        let file1_path = repo_root.join("test_file.txt");
2607        fs::write(&file1_path, b"Test file content").unwrap();
2608        let file1_path_str = file1_path.to_string_lossy().to_string();
2609        let relative1 = file1_path
2610            .strip_prefix(&repo_root)
2611            .unwrap()
2612            .to_string_lossy()
2613            .to_string();
2614
2615        let fa1 = FileAddition::get_or_create(
2616            context.workspace(),
2617            op_conn,
2618            &file1_path_str,
2619            FileTypes::Fasta,
2620            None,
2621        )
2622        .expect("Failed to create FileAddition");
2623
2624        assert_eq!(fa1.file_path, relative1);
2625
2626        let checksum = calculate_file_checksum(&file1_path_str).unwrap();
2627        let relative1_id = FileAddition::generate_file_addition_id(&checksum, &relative1);
2628
2629        assert_eq!(fa1.id, relative1_id);
2630
2631        // Second call with same file should return the same FileAddition
2632        let fa2 = FileAddition::get_or_create(
2633            context.workspace(),
2634            op_conn,
2635            &file1_path_str,
2636            FileTypes::Fasta,
2637            None,
2638        )
2639        .expect("Failed to get existing FileAddition");
2640
2641        assert_eq!(fa1, fa2);
2642
2643        let file2_path = repo_root.join("nested").join("file2.txt");
2644        fs::create_dir_all(file2_path.parent().unwrap()).unwrap();
2645        fs::write(&file2_path, b"Test file content").unwrap();
2646        let file2_path_str = file2_path.to_string_lossy().to_string();
2647
2648        let fa3 = FileAddition::get_or_create(
2649            context.workspace(),
2650            op_conn,
2651            &file2_path_str,
2652            FileTypes::Fasta,
2653            None,
2654        )
2655        .expect("Failed to create different FileAddition");
2656
2657        assert_ne!(fa1.id, fa3.id);
2658
2659        fs::write(&file1_path, b"new content").unwrap();
2660        let fa1_new = FileAddition::get_or_create(
2661            context.workspace(),
2662            op_conn,
2663            &file1_path_str,
2664            FileTypes::Fasta,
2665            None,
2666        )
2667        .expect("Failed to create FileAddition");
2668
2669        assert_ne!(fa1.id, fa1_new.id);
2670    }
2671}