use crate::error::{Result, SpliceError};
use crate::symbol::Language;
use sqlitegraph::NodeId;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendType {
SQLite,
Geometric,
Unknown,
}
impl std::fmt::Display for BackendType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackendType::SQLite => write!(f, "sqlite"),
BackendType::Geometric => write!(f, "geometric"),
BackendType::Unknown => write!(f, "unknown"),
}
}
}
pub enum CodeGraph {
Sqlite(super::sqlite_impl::CodeGraphSqlite),
#[cfg(feature = "geometric")]
Geo(super::geo_impl::CodeGraphGeo),
}
impl CodeGraph {
pub fn open(path: &Path) -> Result<Self> {
if let Ok(metadata) = std::fs::metadata(path) {
if metadata.len() == 0 {
std::fs::remove_file(path).map_err(|e| {
SpliceError::Other(format!(
"Failed to remove empty graph database {:?}: {}",
path, e
))
})?;
}
}
#[cfg(feature = "geometric")]
if Self::is_geometric_db(path) {
return Ok(CodeGraph::Geo(super::geo_impl::CodeGraphGeo::open(path)?));
}
Ok(CodeGraph::Sqlite(
super::sqlite_impl::CodeGraphSqlite::open(path)?,
))
}
pub fn is_geometric_db(path: &Path) -> bool {
path.extension().map_or(false, |ext| ext == "geo")
}
pub fn detect_backend(path: &Path) -> Result<BackendType> {
if Self::is_geometric_db(path) {
#[cfg(feature = "geometric")]
return Ok(BackendType::Geometric);
#[cfg(not(feature = "geometric"))]
return Ok(BackendType::Unknown);
}
if path.exists() {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut header = [0u8; 16];
let bytes_read = file.read(&mut header)?;
if bytes_read >= 16 && header.starts_with(b"SQLite format 3\0") {
return Ok(BackendType::SQLite);
}
if header.starts_with(b"GEODB\0\0\0") {
#[cfg(feature = "geometric")]
return Ok(BackendType::Geometric);
#[cfg(not(feature = "geometric"))]
return Ok(BackendType::Unknown);
}
}
Ok(BackendType::SQLite)
}
pub fn backend_type(&self) -> BackendType {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.backend_type(),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.backend_type(),
}
}
pub fn is_geometric(&self) -> bool {
matches!(self.backend_type(), BackendType::Geometric)
}
pub fn find_symbol_in_file(&self, file_path: &str, name: &str) -> Option<NodeId> {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.find_symbol_in_file(file_path, name),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.find_symbol_in_file(file_path, name),
}
}
pub fn find_symbols_by_name(&self, name: &str) -> Vec<(NodeId, Option<String>)> {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.find_symbols_by_name(name),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.find_symbols_by_name(name),
}
}
pub fn all_symbol_names(&self) -> Vec<String> {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.all_symbol_names(),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.all_symbol_names(),
}
}
pub fn get_span(&self, node_id: NodeId) -> Result<(usize, usize)> {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.get_span(node_id),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.get_span(node_id),
}
}
#[allow(clippy::too_many_arguments)]
pub fn store_symbol(
&mut self,
name: &str,
kind: &str,
language: Language,
byte_start: usize,
byte_end: usize,
line_start: usize,
line_end: usize,
col_start: usize,
col_end: usize,
) -> Result<NodeId> {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.store_symbol(
name, kind, language, byte_start, byte_end, line_start, line_end, col_start,
col_end,
),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.store_symbol(
name, kind, language, byte_start, byte_end, line_start, line_end, col_start,
col_end,
),
}
}
pub fn store_symbol_with_file_and_language(
&mut self,
file_path: &Path,
name: &str,
kind: &str,
language: Language,
byte_start: usize,
byte_end: usize,
line_start: usize,
line_end: usize,
col_start: usize,
col_end: usize,
) -> Result<NodeId> {
match self {
CodeGraph::Sqlite(sqlite) => sqlite.store_symbol_with_file_and_language(
file_path, name, kind, language, byte_start, byte_end, line_start, line_end,
col_start, col_end,
),
#[cfg(feature = "geometric")]
CodeGraph::Geo(geo) => geo.store_symbol_with_file_and_language(
file_path, name, kind, language, byte_start, byte_end, line_start, line_end,
col_start, col_end,
),
}
}
pub fn inner(&self) -> Result<&dyn sqlitegraph::GraphBackend> {
match self {
CodeGraph::Sqlite(sqlite) => Ok(sqlite.inner()),
#[cfg(feature = "geometric")]
CodeGraph::Geo(_) => Err(SpliceError::Other(
"SQLite backend not available. Use geometric() for .geo files".to_string(),
)),
}
}
pub fn inner_mut(&mut self) -> Result<&mut dyn sqlitegraph::GraphBackend> {
match self {
CodeGraph::Sqlite(sqlite) => Ok(sqlite.inner_mut()),
#[cfg(feature = "geometric")]
CodeGraph::Geo(_) => Err(SpliceError::Other(
"SQLite backend not available. Use geometric_mut() for .geo files".to_string(),
)),
}
}
pub fn as_sqlite(&self) -> Option<&super::sqlite_impl::CodeGraphSqlite> {
match self {
CodeGraph::Sqlite(sqlite) => Some(sqlite),
#[cfg(feature = "geometric")]
_ => None,
}
}
pub fn as_sqlite_mut(&mut self) -> Option<&mut super::sqlite_impl::CodeGraphSqlite> {
match self {
CodeGraph::Sqlite(sqlite) => Some(sqlite),
#[cfg(feature = "geometric")]
_ => None,
}
}
#[cfg(feature = "geometric")]
pub fn as_geo(&self) -> Option<&super::geo_impl::CodeGraphGeo> {
match self {
CodeGraph::Geo(geo) => Some(geo),
_ => None,
}
}
#[cfg(feature = "geometric")]
pub fn as_geo_mut(&mut self) -> Option<&mut super::geo_impl::CodeGraphGeo> {
match self {
CodeGraph::Geo(geo) => Some(geo),
_ => None,
}
}
#[cfg(feature = "geometric")]
pub fn geometric(&self) -> Result<&magellan::graph::geometric_backend::GeometricBackend> {
match self {
CodeGraph::Geo(geo) => Ok(geo.inner()),
_ => Err(SpliceError::Other(
"Geometric backend not available. Use inner() for .db files".to_string(),
)),
}
}
#[cfg(feature = "geometric")]
pub fn geometric_mut(
&mut self,
) -> Result<&mut magellan::graph::geometric_backend::GeometricBackend> {
match self {
CodeGraph::Geo(geo) => Ok(geo.inner_mut()),
_ => Err(SpliceError::Other(
"Geometric backend not available. Use inner_mut() for .db files".to_string(),
)),
}
}
pub fn restore_from_snapshot(
_db_path: &Path,
_snapshot_path: &Path,
) -> Result<crate::proof::storage::RestoreResult> {
Err(SpliceError::Other(
"Database snapshot restore is disabled.".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_detect_backend_sqlite() {
let temp_file = NamedTempFile::new().unwrap();
let backend = CodeGraph::detect_backend(temp_file.path()).unwrap();
assert!(matches!(backend, BackendType::SQLite));
}
#[test]
#[cfg(feature = "geometric")]
fn test_detect_backend_geometric() {
let path = Path::new("test.geo");
let backend = CodeGraph::detect_backend(path).unwrap();
assert!(matches!(backend, BackendType::Geometric));
}
#[test]
fn test_is_geometric_db() {
assert!(CodeGraph::is_geometric_db(Path::new("code.geo")));
assert!(!CodeGraph::is_geometric_db(Path::new("code.db")));
assert!(!CodeGraph::is_geometric_db(Path::new(
".magellan/splice.db"
)));
}
#[test]
fn test_backend_type_roundtrip() {
let temp_file = NamedTempFile::new().unwrap();
let graph = CodeGraph::open(temp_file.path()).unwrap();
assert!(matches!(graph.backend_type(), BackendType::SQLite));
}
}