Skip to main content

cqlite_core/storage/
schema_discovery.rs

1//! Schema Discovery and Metadata Management
2//!
3//! This module provides automatic schema detection and metadata management for
4//! SSTable files, enabling the REPL to understand table structures and provide
5//! intelligent data access capabilities.
6
7use 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/// Schema discovery configuration
25#[derive(Debug, Clone)]
26pub struct SchemaDiscoveryConfig {
27    /// Maximum number of rows to sample for type inference
28    pub max_sample_rows: usize,
29    /// Enable aggressive type inference
30    pub aggressive_inference: bool,
31    /// Cache discovered schemas
32    pub cache_schemas: bool,
33    /// Schema cache TTL in seconds
34    pub cache_ttl_seconds: u64,
35    /// Enable schema versioning
36    pub enable_versioning: bool,
37    /// Maximum schema versions to keep
38    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, // 1 hour
48            enable_versioning: true,
49            max_versions: 10,
50        }
51    }
52}
53
54/// Discovered schema information
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DiscoveredSchema {
57    /// Table schema
58    pub schema: TableSchema,
59    /// Discovery metadata
60    pub metadata: SchemaMetadata,
61    /// Column statistics
62    pub column_stats: HashMap<String, ColumnStatistics>,
63    /// Type inference confidence
64    pub inference_confidence: f64,
65    /// Schema validation status
66    pub validation_status: ValidationStatus,
67}
68
69/// Schema discovery metadata
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SchemaMetadata {
72    /// When schema was discovered
73    pub discovered_at: SystemTime,
74    /// Source SSTable files
75    pub source_files: Vec<PathBuf>,
76    /// Number of rows sampled
77    pub rows_sampled: usize,
78    /// Cassandra version detected
79    pub cassandra_version: Option<CassandraVersion>,
80    /// Discovery method used
81    pub discovery_method: DiscoveryMethod,
82    /// Schema version
83    pub version: u32,
84}
85
86/// Column statistics for type inference
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ColumnStatistics {
89    /// Column name
90    pub name: String,
91    /// Inferred data type
92    pub inferred_type: String,
93    /// Type confidence (0.0 - 1.0)
94    pub type_confidence: f64,
95    /// Null value percentage
96    pub null_percentage: f64,
97    /// Unique value count (sampled)
98    pub unique_values: usize,
99    /// Average value size in bytes
100    pub avg_size_bytes: f64,
101    /// Min/max values (for applicable types)
102    pub min_value: Option<Value>,
103    pub max_value: Option<Value>,
104    /// Common patterns detected
105    pub patterns: Vec<String>,
106}
107
108/// Schema validation status
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum ValidationStatus {
111    /// Schema is valid and consistent
112    Valid,
113    /// Schema has minor inconsistencies
114    WarningsPresent,
115    /// Schema has significant issues
116    Invalid,
117    /// Schema could not be validated
118    Unknown,
119}
120
121/// Discovery method used
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub enum DiscoveryMethod {
124    /// From SSTable header metadata
125    HeaderMetadata,
126    /// Inferred from data sampling
127    DataSampling,
128    /// Combination of header and sampling
129    Hybrid,
130    /// From external schema definition
131    External,
132}
133
134/// Schema discovery engine
135#[allow(dead_code)]
136pub struct SchemaDiscovery {
137    /// Configuration
138    config: SchemaDiscoveryConfig,
139    /// Platform abstraction
140    platform: Arc<Platform>,
141    /// Core configuration
142    core_config: Config,
143    /// Schema cache
144    schema_cache: Arc<RwLock<HashMap<String, (DiscoveredSchema, Instant)>>>,
145    /// Type inference engine
146    type_inference: Arc<TypeInferenceEngine>,
147    /// Schema validator
148    validator: Arc<SchemaValidator>,
149}
150
151impl SchemaDiscovery {
152    /// Create a new schema discovery engine
153    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    /// Discover schema for a table from SSTable files
172    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        // Check cache first
181        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        // Perform discovery
188        let discovered = self
189            .perform_schema_discovery(keyspace, table, sstable_files)
190            .await?;
191
192        // Cache the result
193        if self.config.cache_schemas {
194            self.cache_schema(cache_key, discovered.clone()).await;
195        }
196
197        Ok(discovered)
198    }
199
200    /// Perform the actual schema discovery
201    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        // Analyze each SSTable file
214        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                // Try to get schema from header first
219                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                    // Merge with existing column data
226                    self.merge_header_schema(&mut all_column_data, header_schema);
227                }
228
229                // Sample data for type inference
230                let sampled_data = self.sample_table_data(&reader).await?;
231                total_rows_sampled += sampled_data.len();
232
233                // Analyze sampled data
234                self.analyze_sampled_data(&mut all_column_data, sampled_data);
235
236                // Limit total sampling
237                if total_rows_sampled >= self.config.max_sample_rows {
238                    break;
239                }
240            }
241        }
242
243        // Infer final schema
244        let schema = self
245            .infer_table_schema(keyspace, table, &all_column_data)
246            .await?;
247
248        // Calculate column statistics
249        let column_stats = self.calculate_column_statistics(&all_column_data).await;
250
251        // Calculate inference confidence
252        let inference_confidence = self.calculate_inference_confidence(&column_stats);
253
254        // Validate schema
255        let validation_status = self.validator.validate_schema(&schema, &column_stats).await;
256
257        // Determine discovery method
258        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    /// Create a reader for the SSTable file
289    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    /// Extract schema information from SSTable header
294    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    /// Sample data from the SSTable for type inference
309    async fn sample_table_data(
310        &self,
311        reader: &SSTableReader,
312    ) -> Result<Vec<HashMap<String, Value>>> {
313        // Header column names for the raw-fallback case (issue #1334): a
314        // `ScanRow::RawRow` is undecoded whole-row bytes, so — matching the
315        // pre-#1334 sampler — map it onto the first header column rather than a
316        // synthetic `"data"` blob that would infer a bogus column.
317        let header_first_column: Option<String> =
318            reader.header().columns.first().map(|c| c.name.clone());
319
320        // Get all entries and sample up to max_rows
321        let all_entries = reader.get_all_entries().await?;
322
323        // Issue #1334: each entry carries a `ScanRow` row. Disassemble a live row's
324        // interned cells into a name→value map for type inference; map a raw
325        // fallback row onto the header column name; suppress markers (row
326        // tombstone / null row) which carry no columns.
327        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    /// Infer table schema from column data
347    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        // Sort columns by name for consistency
368        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![], // Would need sophisticated analysis
374            clustering_keys: vec![],
375            columns,
376            comments: HashMap::new(),
377            dropped_columns: HashMap::new(),
378        })
379    }
380
381    /// Calculate column statistics
382    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    /// Calculate overall inference confidence
411    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    // Cache management methods
425
426    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        // Simple cache eviction
442        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    // Helper methods for data processing
455
456    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/// Internal column data for analysis
484#[derive(Debug)]
485struct ColumnData {
486    /// Header information if available
487    header_info: Option<ColumnInfo>,
488    /// Sample values collected
489    sample_values: Vec<Value>,
490    /// Unique values (limited set)
491    unique_values: BTreeMap<String, usize>,
492    /// Null count
493    null_count: usize,
494    /// Min/max values
495    min_value: Option<Value>,
496    max_value: Option<Value>,
497    /// Detected patterns
498    detected_patterns: Vec<String>,
499    /// Type frequency map
500    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            // Track type frequency
522            let type_name = value.type_name();
523            *self.type_frequency.entry(type_name).or_insert(0) += 1;
524
525            // Track unique values (limited)
526            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            // Update min/max
532            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
573/// Type inference engine
574struct 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 we have header information, use it
583        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        // Otherwise, infer from sample data
588        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 // Default fallback
598    }
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
626/// Schema validator
627struct 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        // Check type confidence
643        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
662// Extension trait for Value to add helper methods
663trait 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, // JSON estimate
725            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,                   // UDT estimate
730            Value::Frozen(f) => f.estimate_size(), // Recursive
731            Value::Varint(data) => data.len(),
732            Value::Decimal { unscaled, .. } => 4 + unscaled.len(), // scale + data
733            Value::Duration { .. } => 12,                          // 3 * 4 bytes
734            Value::Tombstone(_) => 8,                              // Tombstone marker
735        }
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        // Test that it was created successfully
756        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        // Add some sample values
764        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        // Test calculations
770        assert_eq!(column_data.calculate_null_percentage(), 0.25); // 1 null out of 4 values
771        assert_eq!(column_data.unique_values.len(), 3); // 3 unique text values
772        assert!(column_data.calculate_type_confidence() > 0.7); // Mostly text values
773    }
774}