1use std::collections::{BTreeMap, HashMap};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::{Duration, Instant, SystemTime};
11
12use serde::{Deserialize, Serialize};
13use tokio::sync::RwLock;
14
15use crate::{
16 parser::header::{CassandraVersion, ColumnInfo},
17 platform::Platform,
18 schema::{Column, TableSchema},
19 storage::sstable::reader::SSTableReader,
20 types::{DataType, Value},
21 Config, Result,
22};
23
24#[derive(Debug, Clone)]
26pub struct SchemaDiscoveryConfig {
27 pub max_sample_rows: usize,
29 pub aggressive_inference: bool,
31 pub cache_schemas: bool,
33 pub cache_ttl_seconds: u64,
35 pub enable_versioning: bool,
37 pub max_versions: usize,
39}
40
41impl Default for SchemaDiscoveryConfig {
42 fn default() -> Self {
43 Self {
44 max_sample_rows: 1000,
45 aggressive_inference: true,
46 cache_schemas: true,
47 cache_ttl_seconds: 3600, enable_versioning: true,
49 max_versions: 10,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DiscoveredSchema {
57 pub schema: TableSchema,
59 pub metadata: SchemaMetadata,
61 pub column_stats: HashMap<String, ColumnStatistics>,
63 pub inference_confidence: f64,
65 pub validation_status: ValidationStatus,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SchemaMetadata {
72 pub discovered_at: SystemTime,
74 pub source_files: Vec<PathBuf>,
76 pub rows_sampled: usize,
78 pub cassandra_version: Option<CassandraVersion>,
80 pub discovery_method: DiscoveryMethod,
82 pub version: u32,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ColumnStatistics {
89 pub name: String,
91 pub inferred_type: String,
93 pub type_confidence: f64,
95 pub null_percentage: f64,
97 pub unique_values: usize,
99 pub avg_size_bytes: f64,
101 pub min_value: Option<Value>,
103 pub max_value: Option<Value>,
104 pub patterns: Vec<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum ValidationStatus {
111 Valid,
113 WarningsPresent,
115 Invalid,
117 Unknown,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub enum DiscoveryMethod {
124 HeaderMetadata,
126 DataSampling,
128 Hybrid,
130 External,
132}
133
134#[allow(dead_code)]
136pub struct SchemaDiscovery {
137 config: SchemaDiscoveryConfig,
139 platform: Arc<Platform>,
141 core_config: Config,
143 schema_cache: Arc<RwLock<HashMap<String, (DiscoveredSchema, Instant)>>>,
145 type_inference: Arc<TypeInferenceEngine>,
147 validator: Arc<SchemaValidator>,
149}
150
151impl SchemaDiscovery {
152 pub async fn new(
154 config: SchemaDiscoveryConfig,
155 platform: Arc<Platform>,
156 core_config: Config,
157 ) -> Result<Self> {
158 let type_inference = Arc::new(TypeInferenceEngine::new());
159 let validator = Arc::new(SchemaValidator::new());
160
161 Ok(Self {
162 config,
163 platform,
164 core_config,
165 schema_cache: Arc::new(RwLock::new(HashMap::new())),
166 type_inference,
167 validator,
168 })
169 }
170
171 pub async fn discover_table_schema(
173 &self,
174 keyspace: &str,
175 table: &str,
176 sstable_files: &[PathBuf],
177 ) -> Result<DiscoveredSchema> {
178 let cache_key = format!("{}.{}", keyspace, table);
179
180 if self.config.cache_schemas {
182 if let Some(cached) = self.get_cached_schema(&cache_key).await {
183 return Ok(cached);
184 }
185 }
186
187 let discovered = self
189 .perform_schema_discovery(keyspace, table, sstable_files)
190 .await?;
191
192 if self.config.cache_schemas {
194 self.cache_schema(cache_key, discovered.clone()).await;
195 }
196
197 Ok(discovered)
198 }
199
200 async fn perform_schema_discovery(
202 &self,
203 keyspace: &str,
204 table: &str,
205 sstable_files: &[PathBuf],
206 ) -> Result<DiscoveredSchema> {
207 let start_time = SystemTime::now();
208 let mut source_files = Vec::new();
209 let mut all_column_data = HashMap::new();
210 let mut total_rows_sampled = 0;
211 let mut cassandra_version = None;
212
213 for file_path in sstable_files {
215 if let Ok(reader) = self.create_reader(file_path).await {
216 source_files.push(file_path.clone());
217
218 if let Ok(header_schema) = self.extract_schema_from_header(&reader).await {
220 if cassandra_version.is_none() {
221 let header = reader.header();
222 cassandra_version = Some(header.cassandra_version);
223 }
224
225 self.merge_header_schema(&mut all_column_data, header_schema);
227 }
228
229 let sampled_data = self.sample_table_data(&reader).await?;
231 total_rows_sampled += sampled_data.len();
232
233 self.analyze_sampled_data(&mut all_column_data, sampled_data);
235
236 if total_rows_sampled >= self.config.max_sample_rows {
238 break;
239 }
240 }
241 }
242
243 let schema = self
245 .infer_table_schema(keyspace, table, &all_column_data)
246 .await?;
247
248 let column_stats = self.calculate_column_statistics(&all_column_data).await;
250
251 let inference_confidence = self.calculate_inference_confidence(&column_stats);
253
254 let validation_status = self.validator.validate_schema(&schema, &column_stats).await;
256
257 let discovery_method = if source_files.is_empty() {
259 DiscoveryMethod::External
260 } else if all_column_data.values().any(|cd| cd.header_info.is_some()) {
261 if total_rows_sampled > 0 {
262 DiscoveryMethod::Hybrid
263 } else {
264 DiscoveryMethod::HeaderMetadata
265 }
266 } else {
267 DiscoveryMethod::DataSampling
268 };
269
270 let metadata = SchemaMetadata {
271 discovered_at: start_time,
272 source_files,
273 rows_sampled: total_rows_sampled,
274 cassandra_version,
275 discovery_method,
276 version: 1,
277 };
278
279 Ok(DiscoveredSchema {
280 schema,
281 metadata,
282 column_stats,
283 inference_confidence,
284 validation_status,
285 })
286 }
287
288 async fn create_reader(&self, file_path: &Path) -> Result<SSTableReader> {
290 SSTableReader::open(file_path, &self.core_config, self.platform.clone()).await
291 }
292
293 async fn extract_schema_from_header(
295 &self,
296 reader: &SSTableReader,
297 ) -> Result<HashMap<String, ColumnInfo>> {
298 let header = reader.header();
299 let mut columns = HashMap::new();
300
301 for column_def in &header.columns {
302 columns.insert(column_def.name.clone(), column_def.clone());
303 }
304
305 Ok(columns)
306 }
307
308 async fn sample_table_data(
310 &self,
311 reader: &SSTableReader,
312 ) -> Result<Vec<HashMap<String, Value>>> {
313 let header_first_column: Option<String> =
318 reader.header().columns.first().map(|c| c.name.clone());
319
320 let all_entries = reader.get_all_entries().await?;
322
323 let samples: Vec<HashMap<String, Value>> = all_entries
328 .into_iter()
329 .take(self.config.max_sample_rows)
330 .filter_map(|(_table_id, _row_key, row)| {
331 let cells = row.into_sample_cells(header_first_column.as_deref())?;
332 if cells.is_empty() {
333 return None;
334 }
335 let row_data: HashMap<String, Value> = cells
336 .into_iter()
337 .map(|(name, v)| (name.to_string(), v))
338 .collect();
339 Some(row_data)
340 })
341 .collect();
342
343 Ok(samples)
344 }
345
346 async fn infer_table_schema(
348 &self,
349 keyspace: &str,
350 table: &str,
351 column_data: &HashMap<String, ColumnData>,
352 ) -> Result<TableSchema> {
353 let mut columns = Vec::new();
354
355 for (name, data) in column_data {
356 let data_type = self.type_inference.infer_column_type(data).await;
357 let column = Column {
358 name: name.clone(),
359 data_type: data_type.to_string(),
360 nullable: true,
361 default: None,
362 is_static: false,
363 };
364 columns.push(column);
365 }
366
367 columns.sort_by(|a, b| a.name.cmp(&b.name));
369
370 Ok(TableSchema {
371 keyspace: keyspace.to_string(),
372 table: table.to_string(),
373 partition_keys: vec![], clustering_keys: vec![],
375 columns,
376 comments: HashMap::new(),
377 dropped_columns: HashMap::new(),
378 })
379 }
380
381 async fn calculate_column_statistics(
383 &self,
384 column_data: &HashMap<String, ColumnData>,
385 ) -> HashMap<String, ColumnStatistics> {
386 let mut stats = HashMap::new();
387
388 for (name, data) in column_data {
389 let stat = ColumnStatistics {
390 name: name.clone(),
391 inferred_type: self
392 .type_inference
393 .infer_column_type(data)
394 .await
395 .to_string(),
396 type_confidence: data.calculate_type_confidence(),
397 null_percentage: data.calculate_null_percentage(),
398 unique_values: data.unique_values.len(),
399 avg_size_bytes: data.calculate_average_size(),
400 min_value: data.min_value.clone(),
401 max_value: data.max_value.clone(),
402 patterns: data.detected_patterns.clone(),
403 };
404 stats.insert(name.clone(), stat);
405 }
406
407 stats
408 }
409
410 fn calculate_inference_confidence(
412 &self,
413 column_stats: &HashMap<String, ColumnStatistics>,
414 ) -> f64 {
415 if column_stats.is_empty() {
416 return 0.0;
417 }
418
419 let total_confidence: f64 = column_stats.values().map(|stat| stat.type_confidence).sum();
420
421 total_confidence / column_stats.len() as f64
422 }
423
424 async fn get_cached_schema(&self, cache_key: &str) -> Option<DiscoveredSchema> {
427 let cache = self.schema_cache.read().await;
428 if let Some((schema, cached_at)) = cache.get(cache_key) {
429 let ttl = Duration::from_secs(self.config.cache_ttl_seconds);
430 if cached_at.elapsed() < ttl {
431 return Some(schema.clone());
432 }
433 }
434 None
435 }
436
437 async fn cache_schema(&self, cache_key: String, schema: DiscoveredSchema) {
438 let mut cache = self.schema_cache.write().await;
439 cache.insert(cache_key, (schema, Instant::now()));
440
441 if cache.len() > 100 {
443 let oldest_key = cache
444 .iter()
445 .min_by_key(|(_, (_, time))| time)
446 .map(|(key, _)| key.clone());
447
448 if let Some(key) = oldest_key {
449 cache.remove(&key);
450 }
451 }
452 }
453
454 fn merge_header_schema(
457 &self,
458 column_data: &mut HashMap<String, ColumnData>,
459 header_columns: HashMap<String, ColumnInfo>,
460 ) {
461 for (name, column_info) in header_columns {
462 let entry = column_data.entry(name).or_insert_with(ColumnData::new);
463 entry.header_info = Some(column_info);
464 }
465 }
466
467 fn analyze_sampled_data(
468 &self,
469 column_data: &mut HashMap<String, ColumnData>,
470 samples: Vec<HashMap<String, Value>>,
471 ) {
472 for sample in samples {
473 for (column_name, value) in sample {
474 let entry = column_data
475 .entry(column_name)
476 .or_insert_with(ColumnData::new);
477 entry.add_sample_value(value);
478 }
479 }
480 }
481}
482
483#[derive(Debug)]
485struct ColumnData {
486 header_info: Option<ColumnInfo>,
488 sample_values: Vec<Value>,
490 unique_values: BTreeMap<String, usize>,
492 null_count: usize,
494 min_value: Option<Value>,
496 max_value: Option<Value>,
497 detected_patterns: Vec<String>,
499 type_frequency: HashMap<String, usize>,
501}
502
503impl ColumnData {
504 fn new() -> Self {
505 Self {
506 header_info: None,
507 sample_values: Vec::new(),
508 unique_values: BTreeMap::new(),
509 null_count: 0,
510 min_value: None,
511 max_value: None,
512 detected_patterns: Vec::new(),
513 type_frequency: HashMap::new(),
514 }
515 }
516
517 fn add_sample_value(&mut self, value: Value) {
518 if value == Value::Null {
519 self.null_count += 1;
520 } else {
521 let type_name = value.type_name();
523 *self.type_frequency.entry(type_name).or_insert(0) += 1;
524
525 if self.unique_values.len() < 1000 {
527 let value_str = format!("{:?}", value);
528 *self.unique_values.entry(value_str).or_insert(0) += 1;
529 }
530
531 if self.min_value.is_none() || Some(&value) < self.min_value.as_ref() {
533 self.min_value = Some(value.clone());
534 }
535 if self.max_value.is_none() || Some(&value) > self.max_value.as_ref() {
536 self.max_value = Some(value.clone());
537 }
538
539 self.sample_values.push(value);
540 }
541 }
542
543 fn calculate_type_confidence(&self) -> f64 {
544 if self.type_frequency.is_empty() {
545 return 0.0;
546 }
547
548 let total_samples = self.type_frequency.values().sum::<usize>();
549 let max_frequency = *self.type_frequency.values().max().unwrap_or(&0);
550
551 max_frequency as f64 / total_samples as f64
552 }
553
554 fn calculate_null_percentage(&self) -> f64 {
555 let total = self.sample_values.len() + self.null_count;
556 if total == 0 {
557 0.0
558 } else {
559 self.null_count as f64 / total as f64
560 }
561 }
562
563 fn calculate_average_size(&self) -> f64 {
564 if self.sample_values.is_empty() {
565 0.0
566 } else {
567 let total_size: usize = self.sample_values.iter().map(|v| v.estimate_size()).sum();
568 total_size as f64 / self.sample_values.len() as f64
569 }
570 }
571}
572
573struct TypeInferenceEngine;
575
576impl TypeInferenceEngine {
577 fn new() -> Self {
578 Self
579 }
580
581 async fn infer_column_type(&self, column_data: &ColumnData) -> DataType {
582 if let Some(ref header_info) = column_data.header_info {
584 return self.convert_cql_type_to_data_type(&header_info.column_type);
585 }
586
587 if let Some(most_common_type) = column_data
589 .type_frequency
590 .iter()
591 .max_by_key(|(_, count)| *count)
592 .map(|(type_name, _)| type_name)
593 {
594 return self.string_to_data_type(most_common_type);
595 }
596
597 DataType::Text }
599
600 fn convert_cql_type_to_data_type(&self, type_name: &str) -> DataType {
601 match type_name.to_lowercase().as_str() {
602 "text" | "varchar" | "ascii" => DataType::Text,
603 "int" => DataType::Integer,
604 "bigint" => DataType::BigInt,
605 "boolean" => DataType::Boolean,
606 "double" => DataType::Float,
607 "float" => DataType::Float,
608 "uuid" => DataType::Uuid,
609 "timestamp" => DataType::Timestamp,
610 "blob" => DataType::Blob,
611 _ => DataType::Text,
612 }
613 }
614
615 fn string_to_data_type(&self, type_name: &str) -> DataType {
616 match type_name {
617 "Text" => DataType::Text,
618 "Integer" => DataType::Integer,
619 "Float" => DataType::Float,
620 "Boolean" => DataType::Boolean,
621 _ => DataType::Text,
622 }
623 }
624}
625
626struct SchemaValidator;
628
629impl SchemaValidator {
630 fn new() -> Self {
631 Self
632 }
633
634 async fn validate_schema(
635 &self,
636 _schema: &TableSchema,
637 column_stats: &HashMap<String, ColumnStatistics>,
638 ) -> ValidationStatus {
639 let mut warnings = 0;
640 let mut errors = 0;
641
642 for stat in column_stats.values() {
644 if stat.type_confidence < 0.5 {
645 warnings += 1;
646 }
647 if stat.type_confidence < 0.3 {
648 errors += 1;
649 }
650 }
651
652 if errors > 0 {
653 ValidationStatus::Invalid
654 } else if warnings > 0 {
655 ValidationStatus::WarningsPresent
656 } else {
657 ValidationStatus::Valid
658 }
659 }
660}
661
662trait ValueExt {
664 fn type_name(&self) -> String;
665 fn estimate_size(&self) -> usize;
666}
667
668impl ValueExt for Value {
669 fn type_name(&self) -> String {
670 match self {
671 Value::Null => "Null".to_string(),
672 Value::Text(_) => "Text".to_string(),
673 Value::Integer(_) => "Integer".to_string(),
674 Value::BigInt(_) => "BigInteger".to_string(),
675 Value::Counter(_) => "Counter".to_string(),
676 Value::Float(_) => "Float".to_string(),
677 Value::Boolean(_) => "Boolean".to_string(),
678 Value::Uuid(_) => "UUID".to_string(),
679 Value::Timestamp(_) => "Timestamp".to_string(),
680 Value::Date(_) => "Date".to_string(),
681 Value::Time(_) => "Time".to_string(),
682 Value::Inet(_) => "Inet".to_string(),
683 Value::Blob(_) => "Blob".to_string(),
684 Value::List(_) => "List".to_string(),
685 Value::Set(_) => "Set".to_string(),
686 Value::Map(_) => "Map".to_string(),
687 Value::Json(_) => "JSON".to_string(),
688 Value::TinyInt(_) => "TinyInt".to_string(),
689 Value::SmallInt(_) => "SmallInt".to_string(),
690 Value::Float32(_) => "Float32".to_string(),
691 Value::Tuple(_) => "Tuple".to_string(),
692 Value::Udt(_) => "UDT".to_string(),
693 Value::Frozen(_) => "Frozen".to_string(),
694 Value::Varint(_) => "Varint".to_string(),
695 Value::Decimal { .. } => "Decimal".to_string(),
696 Value::Duration { .. } => "Duration".to_string(),
697 Value::Tombstone(_) => "Tombstone".to_string(),
698 }
699 }
700
701 fn estimate_size(&self) -> usize {
702 match self {
703 Value::Null => 0,
704 Value::Text(s) => s.len(),
705 Value::Integer(_) => 4,
706 Value::BigInt(_) => 8,
707 Value::Counter(_) => 8,
708 Value::Float(_) => 8,
709 Value::Boolean(_) => 1,
710 Value::Uuid(_) => 16,
711 Value::Timestamp(_) => 8,
712 Value::Date(_) => 4,
713 Value::Time(_) => 8,
714 Value::Inet(bytes) => bytes.len(),
715 Value::Blob(b) => b.len(),
716 Value::List(items) => items.iter().map(|v| v.estimate_size()).sum::<usize>() + 8,
717 Value::Set(items) => items.iter().map(|v| v.estimate_size()).sum::<usize>() + 8,
718 Value::Map(map) => {
719 map.iter()
720 .map(|(k, v)| k.estimate_size() + v.estimate_size())
721 .sum::<usize>()
722 + 16
723 }
724 Value::Json(_) => 64, Value::TinyInt(_) => 1,
726 Value::SmallInt(_) => 2,
727 Value::Float32(_) => 4,
728 Value::Tuple(t) => t.iter().map(|v| v.estimate_size()).sum::<usize>() + 8,
729 Value::Udt(_) => 32, Value::Frozen(f) => f.estimate_size(), Value::Varint(data) => data.len(),
732 Value::Decimal { unscaled, .. } => 4 + unscaled.len(), Value::Duration { .. } => 12, Value::Tombstone(_) => 8, }
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use tempfile::TempDir;
743
744 #[tokio::test]
745 async fn test_schema_discovery_creation() {
746 let _temp_dir = TempDir::new().unwrap();
747 let config = SchemaDiscoveryConfig::default();
748 let core_config = Config::default();
749 let platform = Arc::new(Platform::new(&core_config).await.unwrap());
750
751 let discovery = SchemaDiscovery::new(config, platform, core_config)
752 .await
753 .unwrap();
754
755 assert!(!discovery.config.cache_schemas || discovery.schema_cache.read().await.is_empty());
757 }
758
759 #[test]
760 fn test_column_data_analysis() {
761 let mut column_data = ColumnData::new();
762
763 column_data.add_sample_value(Value::text("test1".to_string()));
765 column_data.add_sample_value(Value::text("test2".to_string()));
766 column_data.add_sample_value(Value::Null);
767 column_data.add_sample_value(Value::text("test3".to_string()));
768
769 assert_eq!(column_data.calculate_null_percentage(), 0.25); assert_eq!(column_data.unique_values.len(), 3); assert!(column_data.calculate_type_confidence() > 0.7); }
774}