2026-02-26 15:16:06 -08:00
use chrono ::Utc ;
use rusqlite ::{ params , Connection };
use uuid ::Uuid ;
use super ::errors ::DatabaseError ;
use super ::models ::* ;
// ── Projects ──────────────────────────────────────────────────────
pub fn create_project ( conn : & Connection , name : & str ) -> Result < Project , DatabaseError > {
let id = Uuid ::new_v4 (). to_string ();
let now = Utc ::now (). to_rfc3339 ();
conn . execute (
"INSERT INTO projects (id, name, created_at, updated_at, status) VALUES (?1, ?2, ?3, ?4, 'active')" ,
params! [ id , name , now , now ],
) ? ;
get_project ( conn , & id ) ? . ok_or_else ( || DatabaseError ::NotFound ( "project" . into ()))
}
pub fn get_project ( conn : & Connection , id : & str ) -> Result < Option < Project > , DatabaseError > {
let mut stmt = conn . prepare (
"SELECT id, name, created_at, updated_at, settings, status FROM projects WHERE id = ?1" ,
) ? ;
let mut rows = stmt . query_map ( params! [ id ], | row | {
Ok ( Project {
id : row . get ( 0 ) ? ,
name : row . get ( 1 ) ? ,
created_at : row . get ( 2 ) ? ,
updated_at : row . get ( 3 ) ? ,
settings : row . get ( 4 ) ? ,
status : row . get ( 5 ) ? ,
})
}) ? ;
match rows . next () {
Some ( row ) => Ok ( Some ( row ? )),
None => Ok ( None ),
}
}
pub fn list_projects ( conn : & Connection ) -> Result < Vec < Project > , DatabaseError > {
let mut stmt = conn . prepare (
"SELECT id, name, created_at, updated_at, settings, status FROM projects WHERE status = 'active' ORDER BY updated_at DESC" ,
) ? ;
let rows = stmt . query_map ([], | row | {
Ok ( Project {
id : row . get ( 0 ) ? ,
name : row . get ( 1 ) ? ,
created_at : row . get ( 2 ) ? ,
updated_at : row . get ( 3 ) ? ,
settings : row . get ( 4 ) ? ,
status : row . get ( 5 ) ? ,
})
}) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ >> () ? )
}
pub fn update_project (
conn : & Connection ,
id : & str ,
name : Option <& str > ,
settings : Option <& str > ,
) -> Result < (), DatabaseError > {
let now = Utc ::now (). to_rfc3339 ();
if let Some ( name ) = name {
conn . execute (
"UPDATE projects SET name = ?1, updated_at = ?2 WHERE id = ?3" ,
params! [ name , now , id ],
) ? ;
}
if let Some ( settings ) = settings {
conn . execute (
"UPDATE projects SET settings = ?1, updated_at = ?2 WHERE id = ?3" ,
params! [ settings , now , id ],
) ? ;
}
Ok (())
}
pub fn delete_project ( conn : & Connection , id : & str ) -> Result < (), DatabaseError > {
let now = Utc ::now (). to_rfc3339 ();
conn . execute (
"UPDATE projects SET status = 'deleted', updated_at = ?1 WHERE id = ?2" ,
params! [ now , id ],
) ? ;
Ok (())
}
2026-03-20 22:06:29 -07:00
// ── Media Files ──────────────────────────────────────────────────
pub fn create_media_file (
conn : & Connection ,
project_id : & str ,
file_path : & str ,
) -> Result < MediaFile , DatabaseError > {
let id = Uuid ::new_v4 (). to_string ();
let now = Utc ::now (). to_rfc3339 ();
conn . execute (
"INSERT INTO media_files (id, project_id, file_path, created_at) VALUES (?1, ?2, ?3, ?4)" ,
params! [ id , project_id , file_path , now ],
) ? ;
Ok ( MediaFile {
id ,
project_id : project_id . to_string (),
file_path : file_path . to_string (),
file_hash : None ,
duration_ms : None ,
sample_rate : None ,
channels : None ,
format : None ,
file_size : None ,
created_at : now ,
})
}
pub fn get_media_files_for_project (
conn : & Connection ,
project_id : & str ,
) -> Result < Vec < MediaFile > , DatabaseError > {
let mut stmt = conn . prepare (
"SELECT id, project_id, file_path, file_hash, duration_ms, sample_rate, channels, format, file_size, created_at FROM media_files WHERE project_id = ?1 ORDER BY created_at" ,
) ? ;
let rows = stmt . query_map ( params! [ project_id ], | row | {
Ok ( MediaFile {
id : row . get ( 0 ) ? ,
project_id : row . get ( 1 ) ? ,
file_path : row . get ( 2 ) ? ,
file_hash : row . get ( 3 ) ? ,
duration_ms : row . get ( 4 ) ? ,
sample_rate : row . get ( 5 ) ? ,
channels : row . get ( 6 ) ? ,
format : row . get ( 7 ) ? ,
file_size : row . get ( 8 ) ? ,
created_at : row . get ( 9 ) ? ,
})
}) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ >> () ? )
}
2026-02-26 15:16:06 -08:00
// ── Speakers ──────────────────────────────────────────────────────
pub fn create_speaker (
conn : & Connection ,
project_id : & str ,
label : & str ,
color : Option <& str > ,
) -> Result < Speaker , DatabaseError > {
let id = Uuid ::new_v4 (). to_string ();
conn . execute (
"INSERT INTO speakers (id, project_id, label, color) VALUES (?1, ?2, ?3, ?4)" ,
params! [ id , project_id , label , color ],
) ? ;
Ok ( Speaker {
id ,
project_id : project_id . to_string (),
label : label . to_string (),
display_name : None ,
color : color . map ( String ::from ),
metadata : None ,
})
}
pub fn get_speakers_for_project (
conn : & Connection ,
project_id : & str ,
) -> Result < Vec < Speaker > , DatabaseError > {
let mut stmt = conn . prepare (
"SELECT id, project_id, label, display_name, color, metadata FROM speakers WHERE project_id = ?1" ,
) ? ;
let rows = stmt . query_map ( params! [ project_id ], | row | {
Ok ( Speaker {
id : row . get ( 0 ) ? ,
project_id : row . get ( 1 ) ? ,
label : row . get ( 2 ) ? ,
display_name : row . get ( 3 ) ? ,
color : row . get ( 4 ) ? ,
metadata : row . get ( 5 ) ? ,
})
}) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ >> () ? )
}
pub fn rename_speaker (
conn : & Connection ,
speaker_id : & str ,
display_name : & str ,
) -> Result < (), DatabaseError > {
conn . execute (
"UPDATE speakers SET display_name = ?1 WHERE id = ?2" ,
params! [ display_name , speaker_id ],
) ? ;
Ok (())
}
// ── Segments ──────────────────────────────────────────────────────
pub fn get_segments_for_media (
conn : & Connection ,
media_file_id : & str ,
) -> Result < Vec < Segment > , DatabaseError > {
let mut stmt = conn . prepare (
"SELECT id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, original_text, confidence, is_edited, edited_at, segment_index FROM segments WHERE media_file_id = ?1 ORDER BY segment_index" ,
) ? ;
let rows = stmt . query_map ( params! [ media_file_id ], | row | {
Ok ( Segment {
id : row . get ( 0 ) ? ,
project_id : row . get ( 1 ) ? ,
media_file_id : row . get ( 2 ) ? ,
speaker_id : row . get ( 3 ) ? ,
start_ms : row . get ( 4 ) ? ,
end_ms : row . get ( 5 ) ? ,
text : row . get ( 6 ) ? ,
original_text : row . get ( 7 ) ? ,
confidence : row . get ( 8 ) ? ,
is_edited : row . get ( 9 ) ? ,
edited_at : row . get ( 10 ) ? ,
segment_index : row . get ( 11 ) ? ,
})
}) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ >> () ? )
}
pub fn update_segment_text (
conn : & Connection ,
segment_id : & str ,
new_text : & str ,
) -> Result < (), DatabaseError > {
let now = Utc ::now (). to_rfc3339 ();
// Preserve original text on first edit
conn . execute (
"UPDATE segments SET original_text = COALESCE(original_text, text), text = ?1, is_edited = 1, edited_at = ?2 WHERE id = ?3" ,
params! [ new_text , now , segment_id ],
) ? ;
Ok (())
}
pub fn reassign_speaker (
conn : & Connection ,
segment_id : & str ,
new_speaker_id : & str ,
) -> Result < (), DatabaseError > {
conn . execute (
"UPDATE segments SET speaker_id = ?1 WHERE id = ?2" ,
params! [ new_speaker_id , segment_id ],
) ? ;
Ok (())
}
2026-03-20 22:06:29 -07:00
// ── Segments (create) ────────────────────────────────────────────
pub fn create_segment (
conn : & Connection ,
project_id : & str ,
media_file_id : & str ,
speaker_id : Option <& str > ,
start_ms : i64 ,
end_ms : i64 ,
text : & str ,
segment_index : i32 ,
) -> Result < Segment , DatabaseError > {
let id = Uuid ::new_v4 (). to_string ();
conn . execute (
"INSERT INTO segments (id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, is_edited, segment_index) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, ?8)" ,
params! [ id , project_id , media_file_id , speaker_id , start_ms , end_ms , text , segment_index ],
) ? ;
Ok ( Segment {
id ,
project_id : project_id . to_string (),
media_file_id : media_file_id . to_string (),
speaker_id : speaker_id . map ( String ::from ),
start_ms ,
end_ms ,
text : text . to_string (),
original_text : None ,
confidence : None ,
is_edited : false ,
edited_at : None ,
segment_index ,
})
}
2026-02-26 15:16:06 -08:00
// ── Words ─────────────────────────────────────────────────────────
pub fn get_words_for_segment (
conn : & Connection ,
segment_id : & str ,
) -> Result < Vec < Word > , DatabaseError > {
let mut stmt = conn . prepare (
"SELECT id, segment_id, word, start_ms, end_ms, confidence, word_index FROM words WHERE segment_id = ?1 ORDER BY word_index" ,
) ? ;
let rows = stmt . query_map ( params! [ segment_id ], | row | {
Ok ( Word {
id : row . get ( 0 ) ? ,
segment_id : row . get ( 1 ) ? ,
word : row . get ( 2 ) ? ,
start_ms : row . get ( 3 ) ? ,
end_ms : row . get ( 4 ) ? ,
confidence : row . get ( 5 ) ? ,
word_index : row . get ( 6 ) ? ,
})
}) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ >> () ? )
}
2026-03-20 22:06:29 -07:00
pub fn create_word (
conn : & Connection ,
segment_id : & str ,
word : & str ,
start_ms : i64 ,
end_ms : i64 ,
confidence : Option < f64 > ,
word_index : i32 ,
) -> Result < Word , DatabaseError > {
let id = Uuid ::new_v4 (). to_string ();
conn . execute (
"INSERT INTO words (id, segment_id, word, start_ms, end_ms, confidence, word_index) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" ,
params! [ id , segment_id , word , start_ms , end_ms , confidence , word_index ],
) ? ;
Ok ( Word {
id ,
segment_id : segment_id . to_string (),
word : word . to_string (),
start_ms ,
end_ms ,
confidence ,
word_index ,
})
}
2026-02-26 15:16:06 -08:00
#[cfg(test)]
mod tests {
use super ::* ;
use crate ::db ::schema ;
fn setup_db () -> Connection {
let conn = Connection ::open_in_memory (). unwrap ();
conn . pragma_update ( None , "foreign_keys" , "ON" ). unwrap ();
schema ::create_tables ( & conn ). unwrap ();
conn
}
#[test]
fn test_project_crud () {
let conn = setup_db ();
// Create
let project = create_project ( & conn , "Test Project" ). unwrap ();
assert_eq! ( project . name , "Test Project" );
assert_eq! ( project . status , "active" );
// Read
let fetched = get_project ( & conn , & project . id ). unwrap (). unwrap ();
assert_eq! ( fetched . name , "Test Project" );
// List
let projects = list_projects ( & conn ). unwrap ();
assert_eq! ( projects . len (), 1 );
// Update
update_project ( & conn , & project . id , Some ( "Renamed" ), None ). unwrap ();
let updated = get_project ( & conn , & project . id ). unwrap (). unwrap ();
assert_eq! ( updated . name , "Renamed" );
// Delete (soft)
delete_project ( & conn , & project . id ). unwrap ();
let active = list_projects ( & conn ). unwrap ();
assert_eq! ( active . len (), 0 );
}
#[test]
fn test_speaker_operations () {
let conn = setup_db ();
let project = create_project ( & conn , "Test" ). unwrap ();
let speaker = create_speaker ( & conn , & project . id , "Speaker 1" , Some ( "#ff0000" )). unwrap ();
assert_eq! ( speaker . label , "Speaker 1" );
rename_speaker ( & conn , & speaker . id , "Alice" ). unwrap ();
let speakers = get_speakers_for_project ( & conn , & project . id ). unwrap ();
assert_eq! ( speakers [ 0 ]. display_name . as_deref (), Some ( "Alice" ));
}
#[test]
fn test_segment_text_editing () {
let conn = setup_db ();
let project = create_project ( & conn , "Test" ). unwrap ();
// Insert a media file first
let media_id = Uuid ::new_v4 (). to_string ();
let now = Utc ::now (). to_rfc3339 ();
conn . execute (
"INSERT INTO media_files (id, project_id, file_path, created_at) VALUES (?1, ?2, ?3, ?4)" ,
params! [ media_id , project . id , "test.wav" , now ],
)
. unwrap ();
// Insert a segment
let seg_id = Uuid ::new_v4 (). to_string ();
conn . execute (
"INSERT INTO segments (id, project_id, media_file_id, start_ms, end_ms, text, segment_index) VALUES (?1, ?2, ?3, 0, 1000, 'hello world', 0)" ,
params! [ seg_id , project . id , media_id ],
)
. unwrap ();
// Edit the text
update_segment_text ( & conn , & seg_id , "hello everyone" ). unwrap ();
let segments = get_segments_for_media ( & conn , & media_id ). unwrap ();
assert_eq! ( segments [ 0 ]. text , "hello everyone" );
assert_eq! ( segments [ 0 ]. original_text . as_deref (), Some ( "hello world" ));
assert! ( segments [ 0 ]. is_edited );
}
}