mod analysis;
mod exporter;
mod inference;
mod model;
mod validator;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::RwLock;
use crate::{
parser::header::{CassandraVersion, SSTableHeader},
platform::Platform,
schema::UdtRegistry,
types::Value,
Config, Result,
};
use self::exporter::SchemaExporter;
use self::inference::TypeInferenceEngine;
use self::validator::SchemaValidator;
pub use self::model::{
CachingOptions, CollectionKind, CollectionType, ColumnDefinition, CompactionStrategy,
CompressionOptions, ConsistencyResults, DiscoveryMethod, DiscoveryMetrics, FieldConflict,
IndexDefinition, IndexType, SchemaDiscoveryConfig, SchemaInfo, SchemaMetadata, TableOptions,
TypeInconsistency, TypeInfo, UDTDefinition, UdtConflict, UdtFieldDefinition, UdtFieldInfo,
ValidationError, ValidationErrorType, ValidationResults, ValidationStatus, ValidationWarning,
ValidationWarningType,
};
#[derive(Debug)]
pub struct SchemaDiscoveryEngine {
config: SchemaDiscoveryConfig,
#[allow(dead_code)]
platform: Arc<Platform>,
#[allow(dead_code)]
core_config: Config,
schema_cache: Arc<RwLock<HashMap<String, (SchemaInfo, SystemTime)>>>,
#[allow(dead_code)]
udt_registry: Arc<RwLock<UdtRegistry>>,
#[allow(dead_code)]
type_inference: Arc<TypeInferenceEngine>,
#[allow(dead_code)]
validator: Arc<SchemaValidator>,
exporter: Arc<SchemaExporter>,
}
impl SchemaDiscoveryEngine {
pub async fn new(
config: SchemaDiscoveryConfig,
platform: Arc<Platform>,
core_config: Config,
) -> Result<Self> {
let udt_registry = Arc::new(RwLock::new(UdtRegistry::new()));
let type_inference = Arc::new(TypeInferenceEngine::new());
let validator = Arc::new(SchemaValidator::new());
let exporter = Arc::new(SchemaExporter::new());
Ok(Self {
config,
platform,
core_config,
schema_cache: Arc::new(RwLock::new(HashMap::new())),
udt_registry,
type_inference,
validator,
exporter,
})
}
pub async fn discover_schema(
&self,
keyspace: &str,
table: &str,
sstable_files: &[PathBuf],
) -> Result<SchemaInfo> {
let cache_key = format!("{}.{}", keyspace, table);
let start_time = SystemTime::now();
if self.config.enable_schema_cache {
if let Some(cached_schema) = self.get_cached_schema(&cache_key).await {
return Ok(cached_schema);
}
}
let mut discovery_context = DiscoveryContext::new(keyspace, table, sstable_files);
self.extract_header_metadata(&mut discovery_context).await?;
self.sample_data_for_inference(&mut discovery_context)
.await?;
if self.config.enable_udt_discovery {
self.discover_udts(&mut discovery_context).await?;
}
if self.config.enable_collection_analysis {
self.analyze_collection_types(&mut discovery_context)
.await?;
}
if self.config.enable_index_discovery {
self.discover_indexes(&mut discovery_context).await?;
}
let schema_info = self.build_schema_info(&mut discovery_context).await?;
let validated_schema = schema_info;
let discovery_time = start_time.elapsed().unwrap_or(Duration::ZERO);
let final_schema =
self.add_performance_metrics(validated_schema, discovery_time, &discovery_context);
if self.config.enable_schema_cache {
self.cache_schema(cache_key, final_schema.clone()).await;
}
Ok(final_schema)
}
pub async fn generate_cql(&self, schema: &SchemaInfo) -> Result<String> {
self.exporter.generate_cql(schema).await
}
#[cfg(feature = "experimental")]
pub async fn export_json(&self, schema: &SchemaInfo) -> Result<String> {
self.exporter.export_json(schema).await
}
#[cfg(not(feature = "experimental"))]
pub async fn export_json(&self, _schema: &SchemaInfo) -> Result<String> {
Err(crate::error::Error::unsupported_format(
"JSON export requires experimental feature",
))
}
#[cfg(feature = "experimental")]
pub async fn export_json_with_config(
&self,
schema: &SchemaInfo,
config: &crate::schema::json_exporter::JsonExportConfig,
) -> Result<String> {
self.exporter.export_json_with_config(schema, config).await
}
#[cfg(not(feature = "experimental"))]
pub async fn export_json_with_config<T>(
&self,
_schema: &SchemaInfo,
_config: &T,
) -> Result<String> {
Err(crate::error::Error::unsupported_format(
"JSON export requires experimental feature",
))
}
pub async fn compare_schemas(
&self,
schema1: &SchemaInfo,
schema2: &SchemaInfo,
) -> Result<String> {
self.exporter
.generate_comparison_report(schema1, schema2)
.await
}
async fn get_cached_schema(&self, cache_key: &str) -> Option<SchemaInfo> {
let cache = self.schema_cache.read().await;
if let Some((schema, cached_at)) = cache.get(cache_key) {
let ttl = Duration::from_secs(self.config.cache_ttl_seconds);
if cached_at.elapsed().unwrap_or(Duration::MAX) < ttl {
return Some(schema.clone());
}
}
None
}
async fn cache_schema(&self, cache_key: String, schema: SchemaInfo) {
let mut cache = self.schema_cache.write().await;
cache.insert(cache_key, (schema, SystemTime::now()));
if cache.len() > 100 {
let oldest_key = cache
.iter()
.min_by_key(|(_, (_, time))| time)
.map(|(key, _)| key.clone());
if let Some(key) = oldest_key {
cache.remove(&key);
}
}
}
fn add_performance_metrics(
&self,
mut schema: SchemaInfo,
discovery_time: Duration,
_context: &DiscoveryContext,
) -> SchemaInfo {
schema.metadata.performance_metrics = DiscoveryMetrics {
total_time_ms: discovery_time.as_millis() as u64,
header_parsing_time_ms: 0, data_sampling_time_ms: 0,
type_inference_time_ms: 0,
validation_time_ms: 0,
peak_memory_usage_bytes: 0, };
schema
}
}
#[derive(Debug)]
struct DiscoveryContext {
#[allow(dead_code)]
keyspace: String,
#[allow(dead_code)]
table: String,
#[allow(dead_code)]
source_files: Vec<PathBuf>,
#[allow(dead_code)]
headers: Vec<SSTableHeader>,
#[allow(dead_code)]
column_samples: HashMap<String, Vec<Value>>,
#[allow(dead_code)]
discovered_udts: HashMap<String, UDTDefinition>,
#[allow(dead_code)]
collection_types: HashMap<String, CollectionType>,
#[allow(dead_code)]
indexes: Vec<IndexDefinition>,
#[allow(dead_code)]
table_options: TableOptions,
#[allow(dead_code)]
total_rows_sampled: usize,
#[allow(dead_code)]
cassandra_version: Option<CassandraVersion>,
}
impl DiscoveryContext {
fn new(keyspace: &str, table: &str, files: &[PathBuf]) -> Self {
Self {
keyspace: keyspace.to_string(),
table: table.to_string(),
source_files: files.to_vec(),
headers: Vec::new(),
column_samples: HashMap::new(),
discovered_udts: HashMap::new(),
collection_types: HashMap::new(),
indexes: Vec::new(),
table_options: TableOptions {
compaction: None,
compression: None,
caching: None,
bloom_filter_fp_chance: None,
gc_grace_seconds: None,
default_time_to_live: None,
memtable_flush_period_in_ms: None,
additional_properties: HashMap::new(),
},
total_rows_sampled: 0,
cassandra_version: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_schema_discovery_engine_creation() {
let config = SchemaDiscoveryConfig::default();
let core_config = Config::default();
let platform = Arc::new(Platform::new(&core_config).await.unwrap());
let engine = SchemaDiscoveryEngine::new(config, platform, core_config)
.await
.unwrap();
assert!(engine.schema_cache.read().await.is_empty());
}
#[test]
fn test_discovery_context_creation() {
let files = vec![PathBuf::from("test.sst")];
let context = DiscoveryContext::new("test_ks", "test_table", &files);
assert_eq!(context.keyspace, "test_ks");
assert_eq!(context.table, "test_table");
assert_eq!(context.source_files.len(), 1);
}
#[test]
fn test_schema_info_serialization() {
let schema_info = SchemaInfo {
keyspace: "test".to_string(),
table: "users".to_string(),
partition_key: Vec::new(),
clustering_keys: Vec::new(),
regular_columns: Vec::new(),
static_columns: Vec::new(),
collection_types: HashMap::new(),
user_defined_types: Vec::new(),
indexes: Vec::new(),
table_options: TableOptions {
compaction: None,
compression: None,
caching: None,
bloom_filter_fp_chance: None,
gc_grace_seconds: None,
default_time_to_live: None,
memtable_flush_period_in_ms: None,
additional_properties: HashMap::new(),
},
metadata: SchemaMetadata {
discovered_at: std::time::UNIX_EPOCH,
source_files: Vec::new(),
total_rows_sampled: 0,
cassandra_version: None,
discovery_method: DiscoveryMethod::HeaderMetadata,
version: 1,
validation_results: ValidationResults {
status: ValidationStatus::Valid,
errors: Vec::new(),
warnings: Vec::new(),
consistency_results: ConsistencyResults {
files_analyzed: 0,
schema_mismatches: 0,
type_inconsistencies: Vec::new(),
udt_conflicts: Vec::new(),
},
},
performance_metrics: DiscoveryMetrics {
total_time_ms: 0,
header_parsing_time_ms: 0,
data_sampling_time_ms: 0,
type_inference_time_ms: 0,
validation_time_ms: 0,
peak_memory_usage_bytes: 0,
},
},
};
let json = serde_json::to_string(&schema_info).unwrap();
let deserialized: SchemaInfo = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.keyspace, "test");
assert_eq!(deserialized.table, "users");
}
#[tokio::test]
async fn test_extract_header_metadata_stub() {
let config = SchemaDiscoveryConfig::default();
let core_config = Config::default();
let platform = Arc::new(Platform::new(&core_config).await.unwrap());
let engine = SchemaDiscoveryEngine::new(config, platform, core_config)
.await
.unwrap();
let mut context = DiscoveryContext::new("test_ks", "test_table", &[]);
let result = engine.extract_header_metadata(&mut context).await;
assert!(
result.is_ok(),
"extract_header_metadata stub should return Ok(())"
);
}
#[tokio::test]
async fn test_sample_data_for_inference_stub() {
let config = SchemaDiscoveryConfig::default();
let core_config = Config::default();
let platform = Arc::new(Platform::new(&core_config).await.unwrap());
let engine = SchemaDiscoveryEngine::new(config, platform, core_config)
.await
.unwrap();
let mut context = DiscoveryContext::new("test_ks", "test_table", &[]);
let result = engine.sample_data_for_inference(&mut context).await;
assert!(
result.is_ok(),
"sample_data_for_inference stub should return Ok(())"
);
}
}