1mod analysis;
8mod exporter;
9mod inference;
10mod model;
11mod validator;
12
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::sync::Arc;
16use std::time::{Duration, SystemTime};
17
18use tokio::sync::RwLock;
19
20use crate::{
21 parser::header::{CassandraVersion, SSTableHeader},
22 platform::Platform,
23 schema::UdtRegistry,
24 types::Value,
25 Config, Result,
26};
27
28use self::exporter::SchemaExporter;
29use self::inference::TypeInferenceEngine;
30use self::validator::SchemaValidator;
31
32pub use self::model::{
34 CachingOptions, CollectionKind, CollectionType, ColumnDefinition, CompactionStrategy,
35 CompressionOptions, ConsistencyResults, DiscoveryMethod, DiscoveryMetrics, FieldConflict,
36 IndexDefinition, IndexType, SchemaDiscoveryConfig, SchemaInfo, SchemaMetadata, TableOptions,
37 TypeInconsistency, TypeInfo, UDTDefinition, UdtConflict, UdtFieldDefinition, UdtFieldInfo,
38 ValidationError, ValidationErrorType, ValidationResults, ValidationStatus, ValidationWarning,
39 ValidationWarningType,
40};
41
42#[derive(Debug)]
44pub struct SchemaDiscoveryEngine {
45 config: SchemaDiscoveryConfig,
47 #[allow(dead_code)]
49 platform: Arc<Platform>,
50 #[allow(dead_code)]
52 core_config: Config,
53 schema_cache: Arc<RwLock<HashMap<String, (SchemaInfo, SystemTime)>>>,
55 #[allow(dead_code)]
57 udt_registry: Arc<RwLock<UdtRegistry>>,
58 #[allow(dead_code)]
60 type_inference: Arc<TypeInferenceEngine>,
61 #[allow(dead_code)]
63 validator: Arc<SchemaValidator>,
64 exporter: Arc<SchemaExporter>,
66}
67
68impl SchemaDiscoveryEngine {
69 pub async fn new(
71 config: SchemaDiscoveryConfig,
72 platform: Arc<Platform>,
73 core_config: Config,
74 ) -> Result<Self> {
75 let udt_registry = Arc::new(RwLock::new(UdtRegistry::new()));
76 let type_inference = Arc::new(TypeInferenceEngine::new());
77 let validator = Arc::new(SchemaValidator::new());
78 let exporter = Arc::new(SchemaExporter::new());
79
80 Ok(Self {
81 config,
82 platform,
83 core_config,
84 schema_cache: Arc::new(RwLock::new(HashMap::new())),
85 udt_registry,
86 type_inference,
87 validator,
88 exporter,
89 })
90 }
91
92 pub async fn discover_schema(
94 &self,
95 keyspace: &str,
96 table: &str,
97 sstable_files: &[PathBuf],
98 ) -> Result<SchemaInfo> {
99 let cache_key = format!("{}.{}", keyspace, table);
100 let start_time = SystemTime::now();
101
102 if self.config.enable_schema_cache {
104 if let Some(cached_schema) = self.get_cached_schema(&cache_key).await {
105 return Ok(cached_schema);
106 }
107 }
108
109 let mut discovery_context = DiscoveryContext::new(keyspace, table, sstable_files);
111
112 self.extract_header_metadata(&mut discovery_context).await?;
114
115 self.sample_data_for_inference(&mut discovery_context)
117 .await?;
118
119 if self.config.enable_udt_discovery {
121 self.discover_udts(&mut discovery_context).await?;
122 }
123
124 if self.config.enable_collection_analysis {
126 self.analyze_collection_types(&mut discovery_context)
127 .await?;
128 }
129
130 if self.config.enable_index_discovery {
132 self.discover_indexes(&mut discovery_context).await?;
133 }
134
135 let schema_info = self.build_schema_info(&mut discovery_context).await?;
137
138 let validated_schema = schema_info;
140
141 let discovery_time = start_time.elapsed().unwrap_or(Duration::ZERO);
143 let final_schema =
144 self.add_performance_metrics(validated_schema, discovery_time, &discovery_context);
145
146 if self.config.enable_schema_cache {
148 self.cache_schema(cache_key, final_schema.clone()).await;
149 }
150
151 Ok(final_schema)
152 }
153
154 pub async fn generate_cql(&self, schema: &SchemaInfo) -> Result<String> {
156 self.exporter.generate_cql(schema).await
157 }
158
159 #[cfg(feature = "experimental")]
161 pub async fn export_json(&self, schema: &SchemaInfo) -> Result<String> {
162 self.exporter.export_json(schema).await
163 }
164
165 #[cfg(not(feature = "experimental"))]
166 pub async fn export_json(&self, _schema: &SchemaInfo) -> Result<String> {
167 Err(crate::error::Error::unsupported_format(
168 "JSON export requires experimental feature",
169 ))
170 }
171
172 #[cfg(feature = "experimental")]
174 pub async fn export_json_with_config(
175 &self,
176 schema: &SchemaInfo,
177 config: &crate::schema::json_exporter::JsonExportConfig,
178 ) -> Result<String> {
179 self.exporter.export_json_with_config(schema, config).await
180 }
181
182 #[cfg(not(feature = "experimental"))]
183 pub async fn export_json_with_config<T>(
184 &self,
185 _schema: &SchemaInfo,
186 _config: &T,
187 ) -> Result<String> {
188 Err(crate::error::Error::unsupported_format(
189 "JSON export requires experimental feature",
190 ))
191 }
192
193 pub async fn compare_schemas(
195 &self,
196 schema1: &SchemaInfo,
197 schema2: &SchemaInfo,
198 ) -> Result<String> {
199 self.exporter
200 .generate_comparison_report(schema1, schema2)
201 .await
202 }
203
204 async fn get_cached_schema(&self, cache_key: &str) -> Option<SchemaInfo> {
207 let cache = self.schema_cache.read().await;
208 if let Some((schema, cached_at)) = cache.get(cache_key) {
209 let ttl = Duration::from_secs(self.config.cache_ttl_seconds);
210 if cached_at.elapsed().unwrap_or(Duration::MAX) < ttl {
211 return Some(schema.clone());
212 }
213 }
214 None
215 }
216
217 async fn cache_schema(&self, cache_key: String, schema: SchemaInfo) {
218 let mut cache = self.schema_cache.write().await;
219 cache.insert(cache_key, (schema, SystemTime::now()));
220
221 if cache.len() > 100 {
223 let oldest_key = cache
224 .iter()
225 .min_by_key(|(_, (_, time))| time)
226 .map(|(key, _)| key.clone());
227
228 if let Some(key) = oldest_key {
229 cache.remove(&key);
230 }
231 }
232 }
233
234 fn add_performance_metrics(
235 &self,
236 mut schema: SchemaInfo,
237 discovery_time: Duration,
238 _context: &DiscoveryContext,
239 ) -> SchemaInfo {
240 schema.metadata.performance_metrics = DiscoveryMetrics {
241 total_time_ms: discovery_time.as_millis() as u64,
242 header_parsing_time_ms: 0, data_sampling_time_ms: 0,
244 type_inference_time_ms: 0,
245 validation_time_ms: 0,
246 peak_memory_usage_bytes: 0, };
248 schema
249 }
250}
251
252#[derive(Debug)]
254struct DiscoveryContext {
255 #[allow(dead_code)]
256 keyspace: String,
257 #[allow(dead_code)]
258 table: String,
259 #[allow(dead_code)]
260 source_files: Vec<PathBuf>,
261 #[allow(dead_code)]
262 headers: Vec<SSTableHeader>,
263 #[allow(dead_code)]
264 column_samples: HashMap<String, Vec<Value>>,
265 #[allow(dead_code)]
266 discovered_udts: HashMap<String, UDTDefinition>,
267 #[allow(dead_code)]
268 collection_types: HashMap<String, CollectionType>,
269 #[allow(dead_code)]
270 indexes: Vec<IndexDefinition>,
271 #[allow(dead_code)]
272 table_options: TableOptions,
273 #[allow(dead_code)]
274 total_rows_sampled: usize,
275 #[allow(dead_code)]
276 cassandra_version: Option<CassandraVersion>,
277}
278
279impl DiscoveryContext {
280 fn new(keyspace: &str, table: &str, files: &[PathBuf]) -> Self {
281 Self {
282 keyspace: keyspace.to_string(),
283 table: table.to_string(),
284 source_files: files.to_vec(),
285 headers: Vec::new(),
286 column_samples: HashMap::new(),
287 discovered_udts: HashMap::new(),
288 collection_types: HashMap::new(),
289 indexes: Vec::new(),
290 table_options: TableOptions {
291 compaction: None,
292 compression: None,
293 caching: None,
294 bloom_filter_fp_chance: None,
295 gc_grace_seconds: None,
296 default_time_to_live: None,
297 memtable_flush_period_in_ms: None,
298 additional_properties: HashMap::new(),
299 },
300 total_rows_sampled: 0,
301 cassandra_version: None,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[tokio::test]
311 async fn test_schema_discovery_engine_creation() {
312 let config = SchemaDiscoveryConfig::default();
313 let core_config = Config::default();
314 let platform = Arc::new(Platform::new(&core_config).await.unwrap());
315
316 let engine = SchemaDiscoveryEngine::new(config, platform, core_config)
317 .await
318 .unwrap();
319
320 assert!(engine.schema_cache.read().await.is_empty());
322 }
323
324 #[test]
325 fn test_discovery_context_creation() {
326 let files = vec![PathBuf::from("test.sst")];
327 let context = DiscoveryContext::new("test_ks", "test_table", &files);
328
329 assert_eq!(context.keyspace, "test_ks");
330 assert_eq!(context.table, "test_table");
331 assert_eq!(context.source_files.len(), 1);
332 }
333
334 #[test]
335 fn test_schema_info_serialization() {
336 let schema_info = SchemaInfo {
337 keyspace: "test".to_string(),
338 table: "users".to_string(),
339 partition_key: Vec::new(),
340 clustering_keys: Vec::new(),
341 regular_columns: Vec::new(),
342 static_columns: Vec::new(),
343 collection_types: HashMap::new(),
344 user_defined_types: Vec::new(),
345 indexes: Vec::new(),
346 table_options: TableOptions {
347 compaction: None,
348 compression: None,
349 caching: None,
350 bloom_filter_fp_chance: None,
351 gc_grace_seconds: None,
352 default_time_to_live: None,
353 memtable_flush_period_in_ms: None,
354 additional_properties: HashMap::new(),
355 },
356 metadata: SchemaMetadata {
357 discovered_at: std::time::UNIX_EPOCH,
358 source_files: Vec::new(),
359 total_rows_sampled: 0,
360 cassandra_version: None,
361 discovery_method: DiscoveryMethod::HeaderMetadata,
362 version: 1,
363 validation_results: ValidationResults {
364 status: ValidationStatus::Valid,
365 errors: Vec::new(),
366 warnings: Vec::new(),
367 consistency_results: ConsistencyResults {
368 files_analyzed: 0,
369 schema_mismatches: 0,
370 type_inconsistencies: Vec::new(),
371 udt_conflicts: Vec::new(),
372 },
373 },
374 performance_metrics: DiscoveryMetrics {
375 total_time_ms: 0,
376 header_parsing_time_ms: 0,
377 data_sampling_time_ms: 0,
378 type_inference_time_ms: 0,
379 validation_time_ms: 0,
380 peak_memory_usage_bytes: 0,
381 },
382 },
383 };
384
385 let json = serde_json::to_string(&schema_info).unwrap();
387 let deserialized: SchemaInfo = serde_json::from_str(&json).unwrap();
388 assert_eq!(deserialized.keyspace, "test");
389 assert_eq!(deserialized.table, "users");
390 }
391
392 #[tokio::test]
393 async fn test_extract_header_metadata_stub() {
394 let config = SchemaDiscoveryConfig::default();
395 let core_config = Config::default();
396 let platform = Arc::new(Platform::new(&core_config).await.unwrap());
397
398 let engine = SchemaDiscoveryEngine::new(config, platform, core_config)
399 .await
400 .unwrap();
401
402 let mut context = DiscoveryContext::new("test_ks", "test_table", &[]);
403
404 let result = engine.extract_header_metadata(&mut context).await;
406 assert!(
407 result.is_ok(),
408 "extract_header_metadata stub should return Ok(())"
409 );
410 }
411
412 #[tokio::test]
413 async fn test_sample_data_for_inference_stub() {
414 let config = SchemaDiscoveryConfig::default();
415 let core_config = Config::default();
416 let platform = Arc::new(Platform::new(&core_config).await.unwrap());
417
418 let engine = SchemaDiscoveryEngine::new(config, platform, core_config)
419 .await
420 .unwrap();
421
422 let mut context = DiscoveryContext::new("test_ks", "test_table", &[]);
423
424 let result = engine.sample_data_for_inference(&mut context).await;
426 assert!(
427 result.is_ok(),
428 "sample_data_for_inference stub should return Ok(())"
429 );
430 }
431}