use rusqlite::Connection; use super::error::{LoreError, Result}; use super::path_resolver::escape_like; pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { let exact = conn.query_row( "SELECT id FROM projects WHERE path_with_namespace = ?1", rusqlite::params![project_str], |row| row.get::<_, i64>(0), ); if let Ok(id) = exact { return Ok(id); } let ci = conn.query_row( "SELECT id FROM projects WHERE LOWER(path_with_namespace) = LOWER(?1)", rusqlite::params![project_str], |row| row.get::<_, i64>(0), ); if let Ok(id) = ci { return Ok(id); } let escaped = escape_like(project_str); let mut suffix_stmt = conn.prepare( "SELECT id, path_with_namespace FROM projects WHERE path_with_namespace LIKE '%/' || ?1 ESCAPE '\\' OR path_with_namespace = ?2", )?; let suffix_matches: Vec<(i64, String)> = suffix_stmt .query_map(rusqlite::params![escaped, project_str], |row| { Ok((row.get(0)?, row.get(1)?)) })? .collect::, _>>()?; match suffix_matches.len() { 1 => return Ok(suffix_matches[0].0), n if n > 1 => { let matching: Vec = suffix_matches.iter().map(|(_, p)| p.clone()).collect(); return Err(LoreError::Ambiguous(format!( "Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}", project_str, matching .iter() .map(|p| format!(" {}", p)) .collect::>() .join("\n"), matching[0] ))); } _ => {} } let mut substr_stmt = conn.prepare( "SELECT id, path_with_namespace FROM projects WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%' ESCAPE '\\'", )?; let substr_matches: Vec<(i64, String)> = substr_stmt .query_map(rusqlite::params![escaped], |row| { Ok((row.get(0)?, row.get(1)?)) })? .collect::, _>>()?; match substr_matches.len() { 1 => return Ok(substr_matches[0].0), n if n > 1 => { let matching: Vec = substr_matches.iter().map(|(_, p)| p.clone()).collect(); return Err(LoreError::Ambiguous(format!( "Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}", project_str, matching .iter() .map(|p| format!(" {}", p)) .collect::>() .join("\n"), matching[0] ))); } _ => {} } let mut all_stmt = conn.prepare("SELECT path_with_namespace FROM projects ORDER BY path_with_namespace")?; let all_projects: Vec = all_stmt .query_map([], |row| row.get(0))? .collect::, _>>()?; if all_projects.is_empty() { return Err(LoreError::Other(format!( "Project '{}' not found. No projects have been synced yet.\n\nHint: Run 'lore ingest' first.", project_str ))); } Err(LoreError::Other(format!( "Project '{}' not found.\n\nAvailable projects:\n{}\n\nHint: Use the full path, e.g., --project={}", project_str, all_projects .iter() .map(|p| format!(" {}", p)) .collect::>() .join("\n"), all_projects[0] ))) } /// Escape LIKE metacharacters so `%` and `_` in user input are treated as /// literals. All queries using this must include `ESCAPE '\'`. #[cfg(test)] #[path = "project_tests.rs"] mod tests;