Skip to main content

cqlite_core/schema/
mod.rs

1//! Schema definition and parsing for CQLite
2//!
3//! This module handles schema definitions that describe the structure of
4//! Cassandra tables for schema-aware SSTable reading. It supports both
5//! JSON-based schema definitions and CQL CREATE TABLE statement parsing.
6
7pub mod aggregator;
8pub mod cql_parser;
9pub mod discovery;
10#[cfg(feature = "experimental")]
11pub mod json_exporter;
12pub mod parser;
13pub mod registry;
14
15// Re-export aggregator components
16pub use aggregator::{
17    AggregatorConfig, LoadErrorType, LoadResult, SchemaAggregator, SchemaLoadError,
18    SchemaLoadWarning,
19};
20
21// Re-export CQL parsing functions
22pub use cql_parser::{
23    cql_type_to_type_id, extract_table_name, parse_cql_schema, parse_cql_schema_with_visitor,
24    parse_create_table, table_name_matches,
25};
26
27// Re-export discovery and registry components
28pub use discovery::{
29    ColumnDefinition, DiscoveryMethod, IndexDefinition, SchemaDiscoveryConfig,
30    SchemaDiscoveryEngine, SchemaInfo, SchemaMetadata, TableOptions, TypeInfo, UDTDefinition,
31    ValidationError, ValidationResults, ValidationStatus, ValidationWarning,
32};
33
34pub use registry::{
35    ParsingContext, RegistryStatistics, SchemaChange, SchemaChangeType, SchemaQuery,
36    SchemaRegistry, SchemaRegistryConfig, SchemaSource, SchemaValidationStatus, SchemaValidator,
37    SchemaVersion, ValidationReport,
38};
39
40pub use parser::SchemaParser;
41
42#[cfg(feature = "experimental")]
43pub use json_exporter::{
44    JsonClusteringKey, JsonColumn, JsonExportConfig, JsonExporter, JsonFormat, JsonIndex,
45    JsonMetadata, JsonPerformanceMetrics, JsonPrimaryKey, JsonSchema, JsonTable, JsonTableOptions,
46    JsonUDT, JsonValidationResults,
47};
48
49// Type alias for backward compatibility
50pub type ColumnSpec = Column;
51
52use crate::error::{Error, Result};
53use crate::parser::header::SSTableHeader;
54use crate::parser::types::CqlTypeId;
55use crate::storage::StorageEngine;
56use crate::types::{ComparatorType, UdtTypeDef};
57use crate::Config;
58use serde::{Deserialize, Serialize};
59use std::collections::HashMap;
60use std::fs;
61use std::path::Path;
62use std::sync::Arc;
63use tokio::sync::RwLock;
64
65/// Table schema definition loaded from JSON
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TableSchema {
68    /// Keyspace name
69    pub keyspace: String,
70
71    /// Table name
72    pub table: String,
73
74    /// Partition key columns (ordered)
75    pub partition_keys: Vec<KeyColumn>,
76
77    /// Clustering key columns (ordered)  
78    pub clustering_keys: Vec<ClusteringColumn>,
79
80    /// All columns in the table
81    pub columns: Vec<Column>,
82
83    /// Optional metadata
84    #[serde(default)]
85    pub comments: HashMap<String, String>,
86
87    /// Dropped-column drop times in microseconds (column name → drop_time_micros).
88    ///
89    /// Populated from the schema-loading surface (JSON `dropped_columns`, or set
90    /// programmatically) since drop times are assigned at DDL-execution by the
91    /// cluster catalog (`system_schema.dropped_columns`) and are not recorded in
92    /// local SSTable files or the CQL `DROP COLUMN` text. Used during compaction
93    /// to discard cells of a dropped column whose timestamp ≤ the drop time
94    /// (Cassandra `cb34ad47`). See issues #904 (this plumbing) and #847 (the
95    /// merge-side filter).
96    ///
97    /// Scope (#847): this map carries only the drop time, so the dropped column's
98    /// pre-drop cells are decoded using its CURRENT type in [`Self::columns`] (the
99    /// decode contract enforced by [`Self::validate_dropped_columns`]). That is
100    /// byte-correct when the column's type is unchanged — the common case. A
101    /// column dropped and later RE-ADDED with a DIFFERENT type is out of scope:
102    /// the historical cells would be decoded with the new type and could
103    /// misparse. Supporting per-version types requires carrying type metadata
104    /// here (or decoding from the SSTable serialization-header type) and is
105    /// follow-up work alongside the element-level representation in #899.
106    ///
107    /// Filtering is also at row-timestamp granularity (the merge stream surfaces
108    /// only the row write-time per cell); exact per-cell purging is tracked as
109    /// follow-up #922.
110    #[serde(default)]
111    pub dropped_columns: HashMap<String, i64>,
112}
113
114/// Partition key column definition
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct KeyColumn {
117    /// Column name
118    pub name: String,
119
120    /// CQL data type
121    #[serde(rename = "type")]
122    pub data_type: String,
123
124    /// Position in composite key (0-based)
125    pub position: usize,
126}
127
128/// Clustering key column with ordering
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ClusteringColumn {
131    /// Column name
132    pub name: String,
133
134    /// CQL data type
135    #[serde(rename = "type")]
136    pub data_type: String,
137
138    /// Position in clustering key (0-based)
139    pub position: usize,
140
141    /// Sort order (ASC or DESC)
142    #[serde(default)]
143    pub order: ClusteringOrder,
144}
145
146/// Clustering order enum for sorting
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
148pub enum ClusteringOrder {
149    /// Ascending order
150    #[default]
151    Asc,
152    /// Descending order
153    Desc,
154}
155
156impl From<&str> for ClusteringOrder {
157    fn from(s: &str) -> Self {
158        match s.to_uppercase().as_str() {
159            "DESC" => ClusteringOrder::Desc,
160            _ => ClusteringOrder::Asc,
161        }
162    }
163}
164
165impl std::fmt::Display for ClusteringOrder {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            ClusteringOrder::Asc => write!(f, "ASC"),
169            ClusteringOrder::Desc => write!(f, "DESC"),
170        }
171    }
172}
173
174/// Regular column definition
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct Column {
177    /// Column name
178    pub name: String,
179
180    /// CQL data type (e.g., "text", "bigint", "list<int>")
181    #[serde(rename = "type")]
182    pub data_type: String,
183
184    /// Whether column can be null
185    #[serde(default)]
186    pub nullable: bool,
187
188    /// Default value (if any)
189    #[serde(default)]
190    pub default: Option<serde_json::Value>,
191
192    /// Whether this is a STATIC column
193    #[serde(default)]
194    pub is_static: bool,
195}
196
197/// Parsed CQL data type
198#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
199pub enum CqlType {
200    // Primitive types
201    Boolean,
202    TinyInt,
203    SmallInt,
204    Int,
205    BigInt,
206    Counter,
207    Float,
208    Double,
209    Decimal,
210    Text,
211    Ascii,
212    Varchar,
213    Blob,
214    Timestamp,
215    Date,
216    Time,
217    Uuid,
218    TimeUuid,
219    Inet,
220    Duration,
221    Varint,
222
223    // Collection types (implemented as tuples)
224    List(Box<CqlType>),
225    Set(Box<CqlType>),
226    Map(Box<CqlType>, Box<CqlType>),
227
228    // Complex types
229    Tuple(Vec<CqlType>),
230    Udt(String, Vec<(String, CqlType)>), // name, fields
231    Frozen(Box<CqlType>),
232
233    // Custom/Unknown
234    Custom(String),
235}
236
237/// UDT Schema Registry for managing User Defined Type definitions
238#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239pub struct UdtRegistry {
240    /// Registered UDT type definitions by keyspace and type name
241    udts: HashMap<String, HashMap<String, UdtTypeDef>>,
242}
243
244impl UdtRegistry {
245    /// Create a new UDT registry
246    pub fn new() -> Self {
247        Self {
248            udts: HashMap::new(),
249        }
250    }
251
252    /// Create a new UDT registry with enhanced Cassandra 5.0 defaults
253    pub fn with_cassandra5_defaults() -> Self {
254        let mut registry = Self::new();
255        registry.load_cassandra5_system_udts();
256        registry
257    }
258
259    /// Register a UDT type definition
260    pub fn register_udt(&mut self, udt_def: UdtTypeDef) {
261        let keyspace_udts = self.udts.entry(udt_def.keyspace.clone()).or_default();
262        keyspace_udts.insert(udt_def.name.clone(), udt_def);
263    }
264
265    /// Get a UDT definition by keyspace and name
266    pub fn get_udt(&self, keyspace: &str, name: &str) -> Option<&UdtTypeDef> {
267        self.udts.get(keyspace)?.get(name)
268    }
269
270    /// Get all UDTs in a keyspace
271    pub fn get_keyspace_udts(&self, keyspace: &str) -> Option<&HashMap<String, UdtTypeDef>> {
272        self.udts.get(keyspace)
273    }
274
275    /// List all registered UDT names in a keyspace
276    pub fn list_udt_names(&self, keyspace: &str) -> Vec<&str> {
277        self.udts
278            .get(keyspace)
279            .map(|udts| udts.keys().map(|s| s.as_str()).collect())
280            .unwrap_or_default()
281    }
282
283    /// Check if a UDT is registered
284    pub fn contains_udt(&self, keyspace: &str, name: &str) -> bool {
285        self.udts
286            .get(keyspace)
287            .map(|udts| udts.contains_key(name))
288            .unwrap_or(false)
289    }
290
291    /// Remove a UDT definition
292    pub fn remove_udt(&mut self, keyspace: &str, name: &str) -> Option<UdtTypeDef> {
293        self.udts.get_mut(keyspace)?.remove(name)
294    }
295
296    /// Clear all UDTs in a keyspace
297    pub fn clear_keyspace(&mut self, keyspace: &str) {
298        self.udts.remove(keyspace);
299    }
300
301    /// Get total number of registered UDTs
302    pub fn total_udts(&self) -> usize {
303        self.udts.values().map(|udts| udts.len()).sum()
304    }
305
306    /// Load enhanced Cassandra 5.0 system UDTs with complex nested structures
307    fn load_cassandra5_system_udts(&mut self) {
308        // Enhanced address UDT for Cassandra 5.0 compatibility
309        let address_udt = UdtTypeDef::new("system".to_string(), "address".to_string())
310            .with_field("street".to_string(), CqlType::Text, true)
311            .with_field("street2".to_string(), CqlType::Text, true)
312            .with_field("city".to_string(), CqlType::Text, true)
313            .with_field("state".to_string(), CqlType::Text, true)
314            .with_field("zip_code".to_string(), CqlType::Text, true)
315            .with_field("country".to_string(), CqlType::Text, true)
316            .with_field(
317                "coordinates".to_string(),
318                CqlType::Tuple(vec![CqlType::Double, CqlType::Double]),
319                true,
320            );
321
322        self.register_udt(address_udt);
323
324        // Enhanced person UDT with collections and nested types
325        let person_udt = UdtTypeDef::new("system".to_string(), "person".to_string())
326            .with_field("id".to_string(), CqlType::Uuid, false)
327            .with_field("first_name".to_string(), CqlType::Text, false)
328            .with_field("last_name".to_string(), CqlType::Text, false)
329            .with_field("middle_name".to_string(), CqlType::Text, true)
330            .with_field("age".to_string(), CqlType::Int, true)
331            .with_field("email".to_string(), CqlType::Text, true)
332            .with_field(
333                "phone_numbers".to_string(),
334                CqlType::Set(Box::new(CqlType::Text)),
335                true,
336            )
337            .with_field(
338                "addresses".to_string(),
339                CqlType::List(Box::new(CqlType::Udt("address".to_string(), vec![]))),
340                true,
341            )
342            .with_field(
343                "metadata".to_string(),
344                CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::Text)),
345                true,
346            );
347
348        self.register_udt(person_udt);
349
350        // Contact info UDT for complex nested scenarios
351        let contact_info_udt = UdtTypeDef::new("system".to_string(), "contact_info".to_string())
352            .with_field(
353                "person".to_string(),
354                CqlType::Udt("person".to_string(), vec![]),
355                false,
356            )
357            .with_field(
358                "primary_address".to_string(),
359                CqlType::Udt("address".to_string(), vec![]),
360                true,
361            )
362            .with_field(
363                "emergency_contacts".to_string(),
364                CqlType::List(Box::new(CqlType::Udt("person".to_string(), vec![]))),
365                true,
366            )
367            .with_field("last_updated".to_string(), CqlType::Timestamp, true);
368
369        self.register_udt(contact_info_udt);
370    }
371
372    /// Resolve UDT with full dependency chain
373    pub fn resolve_udt_with_dependencies(
374        &self,
375        keyspace: &str,
376        name: &str,
377    ) -> crate::Result<&UdtTypeDef> {
378        let udt = self.get_udt(keyspace, name).ok_or_else(|| {
379            crate::Error::schema(format!(
380                "UDT '{}' not found in keyspace '{}'",
381                name, keyspace
382            ))
383        })?;
384
385        // Validate all field dependencies exist
386        for field in &udt.fields {
387            self.validate_field_type_dependencies(&field.field_type, keyspace)?;
388        }
389
390        Ok(udt)
391    }
392
393    /// Validate that all UDT field type dependencies exist in the registry
394    fn validate_field_type_dependencies(
395        &self,
396        field_type: &CqlType,
397        keyspace: &str,
398    ) -> crate::Result<()> {
399        match field_type {
400            CqlType::Udt(udt_name, _) => {
401                if !self.contains_udt(keyspace, udt_name) {
402                    return Err(crate::Error::schema(format!(
403                        "UDT dependency '{}' not found in keyspace '{}'",
404                        udt_name, keyspace
405                    )));
406                }
407            }
408            CqlType::List(inner) | CqlType::Set(inner) | CqlType::Frozen(inner) => {
409                self.validate_field_type_dependencies(inner, keyspace)?;
410            }
411            CqlType::Map(key_type, value_type) => {
412                self.validate_field_type_dependencies(key_type, keyspace)?;
413                self.validate_field_type_dependencies(value_type, keyspace)?;
414            }
415            CqlType::Tuple(field_types) => {
416                for tuple_field_type in field_types {
417                    self.validate_field_type_dependencies(tuple_field_type, keyspace)?;
418                }
419            }
420            _ => {} // Primitive types don't need validation
421        }
422        Ok(())
423    }
424
425    /// Get all UDTs that depend on a given UDT (for cascade operations)
426    pub fn get_dependent_udts(&self, keyspace: &str, udt_name: &str) -> Vec<&UdtTypeDef> {
427        let mut dependents = Vec::new();
428
429        if let Some(keyspace_udts) = self.udts.get(keyspace) {
430            for udt in keyspace_udts.values() {
431                if udt.name == udt_name {
432                    continue; // Skip self
433                }
434
435                // Check if this UDT depends on the target UDT
436                if self.udt_depends_on(udt, udt_name) {
437                    dependents.push(udt);
438                }
439            }
440        }
441
442        dependents
443    }
444
445    /// Check if a UDT depends on another UDT (recursively)
446    fn udt_depends_on(&self, udt: &UdtTypeDef, target_udt: &str) -> bool {
447        for field in &udt.fields {
448            if self.field_type_depends_on(&field.field_type, target_udt) {
449                return true;
450            }
451        }
452        false
453    }
454
455    /// Check if a field type depends on a UDT
456    #[allow(clippy::only_used_in_recursion)]
457    fn field_type_depends_on(&self, field_type: &CqlType, target_udt: &str) -> bool {
458        match field_type {
459            CqlType::Udt(udt_name, _) => udt_name == target_udt,
460            CqlType::List(inner) | CqlType::Set(inner) | CqlType::Frozen(inner) => {
461                self.field_type_depends_on(inner, target_udt)
462            }
463            CqlType::Map(key_type, value_type) => {
464                self.field_type_depends_on(key_type, target_udt)
465                    || self.field_type_depends_on(value_type, target_udt)
466            }
467            CqlType::Tuple(field_types) => field_types
468                .iter()
469                .any(|ft| self.field_type_depends_on(ft, target_udt)),
470            _ => false,
471        }
472    }
473
474    /// Register UDT with dependency validation
475    pub fn register_udt_with_validation(&mut self, udt_def: UdtTypeDef) -> crate::Result<()> {
476        // Validate dependencies exist
477        for field in &udt_def.fields {
478            self.validate_field_type_dependencies(&field.field_type, &udt_def.keyspace)?;
479        }
480
481        // Check for circular dependencies
482        if self.would_create_circular_dependency(&udt_def) {
483            return Err(crate::Error::schema(format!(
484                "Registering UDT '{}' would create circular dependency",
485                udt_def.name
486            )));
487        }
488
489        self.register_udt(udt_def);
490        Ok(())
491    }
492
493    /// Check if registering a UDT would create circular dependencies
494    fn would_create_circular_dependency(&self, udt_def: &UdtTypeDef) -> bool {
495        // This is complex - for now, just check direct self-reference
496        for field in &udt_def.fields {
497            if self.field_type_depends_on(&field.field_type, &udt_def.name) {
498                return true;
499            }
500        }
501        false
502    }
503
504    /// Export UDT definitions for debugging
505    pub fn export_definitions(&self, keyspace: &str) -> Vec<String> {
506        let mut definitions = Vec::new();
507
508        if let Some(keyspace_udts) = self.udts.get(keyspace) {
509            for udt in keyspace_udts.values() {
510                let mut def = format!("CREATE TYPE {}.{} (\n", keyspace, udt.name);
511
512                for (i, field) in udt.fields.iter().enumerate() {
513                    if i > 0 {
514                        def.push_str(",\n");
515                    }
516                    def.push_str(&format!(
517                        "  {} {}",
518                        field.name,
519                        self.format_cql_type(&field.field_type)
520                    ));
521                }
522
523                def.push_str("\n);");
524                definitions.push(def);
525            }
526        }
527
528        definitions
529    }
530
531    /// Format CQL type for CREATE TYPE statements
532    #[allow(clippy::only_used_in_recursion)]
533    fn format_cql_type(&self, cql_type: &CqlType) -> String {
534        match cql_type {
535            CqlType::Boolean => "boolean".to_string(),
536            CqlType::TinyInt => "tinyint".to_string(),
537            CqlType::SmallInt => "smallint".to_string(),
538            CqlType::Int => "int".to_string(),
539            CqlType::BigInt => "bigint".to_string(),
540            CqlType::Counter => "counter".to_string(),
541            CqlType::Float => "float".to_string(),
542            CqlType::Double => "double".to_string(),
543            CqlType::Text | CqlType::Varchar => "text".to_string(),
544            CqlType::Ascii => "ascii".to_string(),
545            CqlType::Blob => "blob".to_string(),
546            CqlType::Timestamp => "timestamp".to_string(),
547            CqlType::Date => "date".to_string(),
548            CqlType::Time => "time".to_string(),
549            CqlType::Uuid => "uuid".to_string(),
550            CqlType::TimeUuid => "timeuuid".to_string(),
551            CqlType::Inet => "inet".to_string(),
552            CqlType::Duration => "duration".to_string(),
553            CqlType::Varint => "varint".to_string(),
554            CqlType::Decimal => "decimal".to_string(),
555            CqlType::List(inner) => format!("list<{}>", self.format_cql_type(inner)),
556            CqlType::Set(inner) => format!("set<{}>", self.format_cql_type(inner)),
557            CqlType::Map(key, value) => format!(
558                "map<{}, {}>",
559                self.format_cql_type(key),
560                self.format_cql_type(value)
561            ),
562            CqlType::Udt(name, _) => name.clone(),
563            CqlType::Tuple(types) => {
564                let type_strs: Vec<String> =
565                    types.iter().map(|t| self.format_cql_type(t)).collect();
566                format!("tuple<{}>", type_strs.join(", "))
567            }
568            CqlType::Frozen(inner) => format!("frozen<{}>", self.format_cql_type(inner)),
569            CqlType::Custom(name) => name.clone(),
570        }
571    }
572}
573
574/// Whether a de-prefixed `Custom` type name is a plausible UDT reference.
575///
576/// `CqlType::parse` returns `Custom(..)` both for real UDT names and for type
577/// strings it cannot structurally parse (e.g. an uppercase `SET<TEXT>` whose
578/// collection prefix it doesn't recognize). Only simple identifiers
579/// (alphanumeric / `_` / `.`) can name a UDT, so structural fragments
580/// containing `<`, `>`, `,` or whitespace are excluded from UDT validation.
581pub(crate) fn is_udt_identifier(name: &str) -> bool {
582    !name.is_empty()
583        && name
584            .chars()
585            .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
586}
587
588impl TableSchema {
589    /// Extract schema from SSTable header column metadata
590    ///
591    /// This method constructs a TableSchema from the column information
592    /// embedded in the SSTable header's SerializationHeader.
593    pub fn from_sstable_header(header: &SSTableHeader) -> Result<Self> {
594        // Separate columns by role
595        let mut partition_keys = Vec::new();
596        let mut clustering_keys = Vec::new();
597        let mut regular_columns = Vec::new();
598
599        for col_info in &header.columns {
600            if col_info.is_primary_key {
601                if col_info.is_clustering {
602                    clustering_keys.push(col_info);
603                } else {
604                    partition_keys.push(col_info);
605                }
606            } else {
607                regular_columns.push(col_info);
608            }
609        }
610
611        // Validate all partition keys have positions
612        for col_info in &partition_keys {
613            if col_info.key_position.is_none() {
614                return Err(Error::schema(format!(
615                    "Partition key column '{}' missing key_position in SSTable header",
616                    col_info.name
617                )));
618            }
619        }
620
621        // Validate all clustering keys have positions
622        for col_info in &clustering_keys {
623            if col_info.key_position.is_none() {
624                return Err(Error::schema(format!(
625                    "Clustering key column '{}' missing key_position in SSTable header",
626                    col_info.name
627                )));
628            }
629        }
630
631        // Sort by header's key_position to establish canonical ordering
632        partition_keys.sort_by_key(|c| c.key_position.unwrap());
633        clustering_keys.sort_by_key(|c| c.key_position.unwrap());
634
635        // Build KeyColumn with contiguous 0-based positions for CQLite's internal representation
636        // (SSTable key_position values may have gaps; we normalize to [0,1,2,...])
637        let partition_keys: Vec<KeyColumn> = partition_keys
638            .iter()
639            .enumerate()
640            .map(|(pos, col)| KeyColumn {
641                name: col.name.clone(),
642                data_type: col.column_type.clone(),
643                position: pos, // Contiguous internal position, not header key_position
644            })
645            .collect();
646
647        // Build ClusteringColumn with contiguous positions
648        let clustering_keys: Vec<ClusteringColumn> = clustering_keys
649            .iter()
650            .enumerate()
651            .map(|(pos, col)| ClusteringColumn {
652                name: col.name.clone(),
653                data_type: col.column_type.clone(),
654                position: pos, // Contiguous internal position, not header key_position
655                // Issue #759: the serialization header wraps a DESC clustering
656                // column's comparator in `ReversedType(...)`. That authoritative
657                // signal is captured in `ColumnInfo::clustering_reversed` during
658                // Statistics.db parsing (no heuristics). `data_type` already holds
659                // the unwrapped inner CQL type, so deserialization is undisturbed.
660                order: if col.clustering_reversed {
661                    ClusteringOrder::Desc
662                } else {
663                    ClusteringOrder::Asc
664                },
665            })
666            .collect();
667
668        // All columns including keys
669        let columns: Vec<Column> = header
670            .columns
671            .iter()
672            .map(|col| Column {
673                name: col.name.clone(),
674                data_type: col.column_type.clone(),
675                nullable: !col.is_primary_key, // Primary keys are non-nullable
676                default: None,
677                // Static-column classification is authoritative metadata from the
678                // Statistics.db SerializationHeader (definitive guide Ch.7 / Appendix B),
679                // surfaced on ColumnInfo.is_static. Issue #758 / Epic #756.
680                is_static: col.is_static,
681            })
682            .collect();
683
684        if partition_keys.is_empty() {
685            return Err(Error::schema(
686                "No partition keys found in SSTable header".to_string(),
687            ));
688        }
689
690        let schema = TableSchema {
691            keyspace: header.keyspace.clone(),
692            table: header.table_name.clone(),
693            partition_keys,
694            clustering_keys,
695            columns,
696            comments: HashMap::new(),
697            dropped_columns: HashMap::new(),
698        };
699
700        schema.validate()?;
701        Ok(schema)
702    }
703
704    /// Load schema from JSON file
705    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
706        let content = fs::read_to_string(path)
707            .map_err(|e| Error::schema(format!("Failed to read schema file: {}", e)))?;
708
709        Self::from_json(&content)
710    }
711
712    /// Parse schema from JSON string
713    pub fn from_json(json: &str) -> Result<Self> {
714        let schema: TableSchema = serde_json::from_str(json)
715            .map_err(|e| Error::schema(format!("Invalid JSON schema: {}", e)))?;
716
717        schema.validate()?;
718        Ok(schema)
719    }
720
721    /// Save schema to JSON file
722    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
723        let json = serde_json::to_string_pretty(self)
724            .map_err(|e| Error::serialization(format!("Failed to serialize schema: {}", e)))?;
725
726        fs::write(path, json)
727            .map_err(|e| Error::schema(format!("Failed to write schema file: {}", e)))?;
728
729        Ok(())
730    }
731
732    /// Validate schema consistency
733    pub fn validate(&self) -> Result<()> {
734        // Validate keyspace and table names
735        if self.keyspace.is_empty() {
736            return Err(Error::schema("Keyspace name cannot be empty".to_string()));
737        }
738
739        if self.table.is_empty() {
740            return Err(Error::schema("Table name cannot be empty".to_string()));
741        }
742
743        // Must have at least one partition key
744        if self.partition_keys.is_empty() {
745            return Err(Error::schema(
746                "Table must have at least one partition key".to_string(),
747            ));
748        }
749
750        // Validate partition key positions are contiguous
751        let mut positions: Vec<_> = self.partition_keys.iter().map(|k| k.position).collect();
752        positions.sort();
753        for (i, &pos) in positions.iter().enumerate() {
754            if pos != i {
755                return Err(Error::schema(format!(
756                    "Partition key positions must be contiguous starting from 0, found gap at position {}",
757                    i
758                )));
759            }
760        }
761
762        // Validate clustering key positions (if any)
763        if !self.clustering_keys.is_empty() {
764            let mut positions: Vec<_> = self.clustering_keys.iter().map(|k| k.position).collect();
765            positions.sort();
766            for (i, &pos) in positions.iter().enumerate() {
767                if pos != i {
768                    return Err(Error::schema(format!(
769                        "Clustering key positions must be contiguous starting from 0, found gap at position {}",
770                        i
771                    )));
772                }
773            }
774        }
775
776        // Validate data types
777        for column in &self.columns {
778            CqlType::parse(&column.data_type).map_err(|e| {
779                Error::schema(format!(
780                    "Invalid data type '{}' for column '{}': {}",
781                    column.data_type, column.name, e
782                ))
783            })?;
784        }
785
786        // NOTE: UDT-reference validation requires a registry and is not done here
787        // (a TableSchema is self-contained). At schema-load time, call
788        // `validate_udt_references(&registry)` to fail fast on undefined UDTs.
789
790        // Validate all key columns exist in columns list
791        for key in &self.partition_keys {
792            if !self.columns.iter().any(|c| c.name == key.name) {
793                return Err(Error::schema(format!(
794                    "Partition key '{}' not found in columns list",
795                    key.name
796                )));
797            }
798        }
799
800        for key in &self.clustering_keys {
801            if !self.columns.iter().any(|c| c.name == key.name) {
802                return Err(Error::schema(format!(
803                    "Clustering key '{}' not found in columns list",
804                    key.name
805                )));
806            }
807        }
808
809        self.validate_dropped_columns()?;
810
811        Ok(())
812    }
813
814    /// The **post-drop** schema that compaction uses to *write* its output.
815    ///
816    /// The decode schema retains dropped columns (carrying their type) so input
817    /// cells can be parsed and then purged by the merge filter (see
818    /// [`Self::validate_dropped_columns`]). The compaction *output* must keep its
819    /// serialization header / row column bitmap consistent with the cells that
820    /// actually survive the merge:
821    ///
822    /// - A dropped column with **no surviving cells** (all cells were at or
823    ///   before its drop time) is removed from `columns` so it does not appear in
824    ///   the output header. This lets a natural post-drop reader schema (which
825    ///   omits the column) read the output without the header-column /
826    ///   bitmap-index misalignment that retaining it would cause (roborev #847).
827    /// - A dropped column with **surviving cells** (re-added: cells written after
828    ///   `drop_time`) is RETAINED in `columns`, because the merge still emits
829    ///   those cells and the writer needs a matching header column — otherwise
830    ///   the cell would be serialized with no header entry and corrupt the row.
831    ///
832    /// `retained` is the set of dropped-column names that had surviving cells
833    /// (computed by `compact_sstables` from a merge pre-pass). The returned
834    /// schema carries an empty `dropped_columns` map: the purge has already
835    /// happened, so the output must not re-purge the surviving cells on a later
836    /// compaction.
837    pub fn for_compaction_output(
838        &self,
839        retained: &std::collections::HashSet<String>,
840    ) -> TableSchema {
841        TableSchema {
842            keyspace: self.keyspace.clone(),
843            table: self.table.clone(),
844            partition_keys: self.partition_keys.clone(),
845            clustering_keys: self.clustering_keys.clone(),
846            columns: self
847                .columns
848                .iter()
849                .filter(|c| {
850                    // Keep a column unless it is a dropped column with no
851                    // surviving cells.
852                    !self.dropped_columns.contains_key(&c.name) || retained.contains(&c.name)
853                })
854                .cloned()
855                .collect(),
856            comments: self.comments.clone(),
857            dropped_columns: HashMap::new(),
858        }
859    }
860
861    /// Validate the dropped-column decode contract (#904/#847).
862    ///
863    /// Dropped-column filtering during compaction discards a dropped column's
864    /// cells *after they are decoded*. The schema-driven reader only decodes a
865    /// column whose name is present in [`Self::columns`] (it intersects the
866    /// on-disk serialization-header columns with the schema); a column absent
867    /// from `columns` is skipped without consuming its bytes, so its cells would
868    /// never reach the filter and surrounding columns could misalign.
869    ///
870    /// Therefore every column named in `dropped_columns` MUST remain declared in
871    /// `columns` (carrying its type) so its cells decode and can be purged. This
872    /// mirrors Cassandra retaining a dropped column's type in
873    /// `system_schema.dropped_columns`. Decoding a dropped column that is absent
874    /// from `columns` (purely from header type metadata) is follow-up work
875    /// related to #899 and intentionally out of scope here.
876    pub fn validate_dropped_columns(&self) -> Result<()> {
877        for name in self.dropped_columns.keys() {
878            if !self.columns.iter().any(|c| &c.name == name) {
879                return Err(Error::schema(format!(
880                    "dropped column '{}' must remain declared in `columns` (with its type) so \
881                     its cells can be decoded and purged during compaction; a dropped column \
882                     present only in `dropped_columns` cannot be decoded (see #904/#847)",
883                    name
884                )));
885            }
886        }
887        Ok(())
888    }
889
890    /// Validate that every UDT referenced by a column exists in the registry.
891    ///
892    /// This is a schema-load-time pass that fails fast with a schema-category
893    /// error naming the missing UDT, instead of surfacing the problem later as a
894    /// confusing parse/deserialization error (issue #761). Nested references are
895    /// validated recursively: a UDT inside a collection, `frozen<>`, a tuple, or
896    /// another UDT is checked just like a top-level reference.
897    ///
898    /// UDTs are looked up in the schema's own keyspace; the `system` keyspace is
899    /// also consulted so built-in/system UDTs resolve regardless of the table's
900    /// keyspace.
901    pub fn validate_udt_references(&self, registry: &UdtRegistry) -> Result<()> {
902        for column in &self.columns {
903            // Reuse the same parse the rest of validation uses; parse errors are
904            // reported by `validate()`, so ignore them here.
905            if let Ok(cql_type) = CqlType::parse(&column.data_type) {
906                self.check_type_udt_references(&cql_type, &column.name, registry)?;
907            }
908        }
909        Ok(())
910    }
911
912    /// Recursively check a single CQL type for UDT references that are not
913    /// present in the registry.
914    fn check_type_udt_references(
915        &self,
916        cql_type: &CqlType,
917        column_name: &str,
918        registry: &UdtRegistry,
919    ) -> Result<()> {
920        match cql_type {
921            CqlType::Udt(name, _) => {
922                self.ensure_udt_exists(name, column_name, registry)?;
923            }
924            // `CqlType::parse` represents UDT references as `Custom("udt:<name>")`
925            // for names with mixed case / underscores / digits, but as a bare
926            // `Custom("<name>")` for purely-lowercase names (e.g. `address`).
927            // Validate the de-prefixed name *only* when it is a simple type
928            // identifier: `parse` also yields a bare `Custom` for type strings it
929            // can't structurally parse (e.g. an uppercase `SET<TEXT>` collection),
930            // which must NOT be mistaken for a UDT (roborev job 39 + the
931            // collections-fixture regression).
932            CqlType::Custom(name) => {
933                let udt_name = name.strip_prefix("udt:").unwrap_or(name);
934                if is_udt_identifier(udt_name) {
935                    self.ensure_udt_exists(udt_name, column_name, registry)?;
936                }
937            }
938            CqlType::List(inner) | CqlType::Set(inner) | CqlType::Frozen(inner) => {
939                self.check_type_udt_references(inner, column_name, registry)?;
940            }
941            CqlType::Map(key_type, value_type) => {
942                self.check_type_udt_references(key_type, column_name, registry)?;
943                self.check_type_udt_references(value_type, column_name, registry)?;
944            }
945            CqlType::Tuple(field_types) => {
946                for field_type in field_types {
947                    self.check_type_udt_references(field_type, column_name, registry)?;
948                }
949            }
950            _ => {} // Primitive types reference no UDTs.
951        }
952        Ok(())
953    }
954
955    /// Confirm a referenced UDT exists in the table's keyspace (or the `system`
956    /// keyspace), returning a schema-category error naming the missing UDT.
957    fn ensure_udt_exists(
958        &self,
959        udt_name: &str,
960        column_name: &str,
961        registry: &UdtRegistry,
962    ) -> Result<()> {
963        // A reference may be qualified as `keyspace.udt`; honor an explicit
964        // keyspace, otherwise resolve against the table's keyspace.
965        let (lookup_keyspace, bare_name) = match udt_name.split_once('.') {
966            Some((ks, name)) => (ks, name),
967            None => (self.keyspace.as_str(), udt_name),
968        };
969
970        if registry.contains_udt(lookup_keyspace, bare_name)
971            || registry.contains_udt("system", bare_name)
972        {
973            return Ok(());
974        }
975
976        Err(Error::schema(format!(
977            "Column '{}' references undefined UDT '{}' in keyspace '{}'",
978            column_name, udt_name, lookup_keyspace
979        )))
980    }
981
982    /// Get column by name
983    pub fn get_column(&self, name: &str) -> Option<&Column> {
984        self.columns.iter().find(|c| c.name == name)
985    }
986
987    /// Check if column is a partition key
988    pub fn is_partition_key(&self, name: &str) -> bool {
989        self.partition_keys.iter().any(|k| k.name == name)
990    }
991
992    /// Check if column is a clustering key
993    pub fn is_clustering_key(&self, name: &str) -> bool {
994        self.clustering_keys.iter().any(|k| k.name == name)
995    }
996
997    /// Get partition key columns in order
998    pub fn ordered_partition_keys(&self) -> Vec<&KeyColumn> {
999        let mut keys = self.partition_keys.iter().collect::<Vec<_>>();
1000        keys.sort_by_key(|k| k.position);
1001        keys
1002    }
1003
1004    /// Get clustering key columns in order
1005    pub fn ordered_clustering_keys(&self) -> Vec<&ClusteringColumn> {
1006        let mut keys = self.clustering_keys.iter().collect::<Vec<_>>();
1007        keys.sort_by_key(|k| k.position);
1008        keys
1009    }
1010
1011    /// Get ComparatorType for a specific column
1012    pub fn get_column_comparator(&self, column_name: &str) -> Result<ComparatorType> {
1013        let column = self
1014            .get_column(column_name)
1015            .ok_or_else(|| Error::Schema(format!("Column '{}' not found", column_name)))?;
1016
1017        let cql_type = CqlType::parse(&column.data_type)?;
1018        ComparatorType::from_cql_type(&cql_type)
1019    }
1020
1021    /// Get ComparatorTypes for all columns
1022    pub fn get_all_comparators(&self) -> Result<HashMap<String, ComparatorType>> {
1023        let mut comparators = HashMap::new();
1024
1025        for column in &self.columns {
1026            let cql_type = CqlType::parse(&column.data_type)?;
1027            let comparator = ComparatorType::from_cql_type(&cql_type)?;
1028            comparators.insert(column.name.clone(), comparator);
1029        }
1030
1031        Ok(comparators)
1032    }
1033
1034    /// Get ComparatorTypes for partition key columns in order
1035    pub fn get_partition_key_comparators(&self) -> Result<Vec<ComparatorType>> {
1036        let mut comparators = Vec::new();
1037        let ordered_keys = self.ordered_partition_keys();
1038
1039        for key_column in ordered_keys {
1040            let cql_type = CqlType::parse(&key_column.data_type)?;
1041            let comparator = ComparatorType::from_cql_type(&cql_type)?;
1042            comparators.push(comparator);
1043        }
1044
1045        Ok(comparators)
1046    }
1047
1048    /// Get ComparatorTypes for clustering key columns in order
1049    pub fn get_clustering_key_comparators(&self) -> Result<Vec<ComparatorType>> {
1050        let mut comparators = Vec::new();
1051        let ordered_keys = self.ordered_clustering_keys();
1052
1053        for key_column in ordered_keys {
1054            let cql_type = CqlType::parse(&key_column.data_type)?;
1055            let comparator = ComparatorType::from_cql_type(&cql_type)?;
1056            comparators.push(comparator);
1057        }
1058
1059        Ok(comparators)
1060    }
1061
1062    /// Check if a column type is compatible with an expected type
1063    pub fn is_column_type_compatible(
1064        &self,
1065        column_name: &str,
1066        expected_type: &str,
1067    ) -> Result<bool> {
1068        let column_comparator = self.get_column_comparator(column_name)?;
1069        let expected_cql_type = CqlType::parse(expected_type)?;
1070        let expected_comparator = ComparatorType::from_cql_type(&expected_cql_type)?;
1071
1072        Ok(self.comparators_are_compatible(&column_comparator, &expected_comparator))
1073    }
1074
1075    /// Check if two ComparatorTypes are compatible (helper method)
1076    #[allow(clippy::only_used_in_recursion)]
1077    fn comparators_are_compatible(&self, left: &ComparatorType, right: &ComparatorType) -> bool {
1078        match (left, right) {
1079            // Exact matches
1080            (ComparatorType::Boolean, ComparatorType::Boolean) => true,
1081            (ComparatorType::TinyInt, ComparatorType::TinyInt) => true,
1082            (ComparatorType::SmallInt, ComparatorType::SmallInt) => true,
1083            (ComparatorType::Int, ComparatorType::Int) => true,
1084            (ComparatorType::BigInt, ComparatorType::BigInt) => true,
1085            (ComparatorType::Float32, ComparatorType::Float32) => true,
1086            (ComparatorType::Float, ComparatorType::Float) => true,
1087            (ComparatorType::Text, ComparatorType::Text) => true,
1088            (ComparatorType::Blob, ComparatorType::Blob) => true,
1089            (ComparatorType::Timestamp, ComparatorType::Timestamp) => true,
1090            (ComparatorType::Uuid, ComparatorType::Uuid) => true,
1091            (ComparatorType::Json, ComparatorType::Json) => true,
1092
1093            // Collection types
1094            (ComparatorType::List(l_elem), ComparatorType::List(r_elem)) => {
1095                self.comparators_are_compatible(l_elem, r_elem)
1096            }
1097            (ComparatorType::Set(l_elem), ComparatorType::Set(r_elem)) => {
1098                self.comparators_are_compatible(l_elem, r_elem)
1099            }
1100            (ComparatorType::Map(l_key, l_val), ComparatorType::Map(r_key, r_val)) => {
1101                self.comparators_are_compatible(l_key, r_key)
1102                    && self.comparators_are_compatible(l_val, r_val)
1103            }
1104
1105            // Tuple types
1106            (ComparatorType::Tuple(l_fields), ComparatorType::Tuple(r_fields)) => {
1107                l_fields.len() == r_fields.len()
1108                    && l_fields
1109                        .iter()
1110                        .zip(r_fields.iter())
1111                        .all(|(l, r)| self.comparators_are_compatible(l, r))
1112            }
1113
1114            // UDT types
1115            (
1116                ComparatorType::Udt {
1117                    type_name: l_name,
1118                    keyspace: l_ks,
1119                    ..
1120                },
1121                ComparatorType::Udt {
1122                    type_name: r_name,
1123                    keyspace: r_ks,
1124                    ..
1125                },
1126            ) => l_name == r_name && l_ks == r_ks,
1127
1128            // Frozen types
1129            (ComparatorType::Frozen(l_inner), ComparatorType::Frozen(r_inner)) => {
1130                self.comparators_are_compatible(l_inner, r_inner)
1131            }
1132
1133            // Custom types
1134            (ComparatorType::Custom(l_name), ComparatorType::Custom(r_name)) => l_name == r_name,
1135
1136            // No other combinations are compatible
1137            _ => false,
1138        }
1139    }
1140
1141    /// Create a minimal test schema (for testing only)
1142    #[cfg(test)]
1143    pub fn new_for_testing(keyspace: &str, table: &str) -> Self {
1144        Self {
1145            keyspace: keyspace.to_string(),
1146            table: table.to_string(),
1147            partition_keys: vec![KeyColumn {
1148                name: "id".to_string(),
1149                data_type: "int".to_string(),
1150                position: 0,
1151            }],
1152            clustering_keys: vec![],
1153            columns: vec![Column {
1154                name: "id".to_string(),
1155                data_type: "int".to_string(),
1156                nullable: false,
1157                default: None,
1158                is_static: false,
1159            }],
1160            comments: HashMap::new(),
1161            dropped_columns: HashMap::new(),
1162        }
1163    }
1164}
1165
1166impl CqlType {
1167    fn split_top_level_types(type_str: &str) -> Result<Vec<&str>> {
1168        let mut parts = Vec::new();
1169        let mut depth = 0usize;
1170        let mut start = 0usize;
1171
1172        for (index, ch) in type_str.char_indices() {
1173            match ch {
1174                '<' => depth += 1,
1175                '>' => {
1176                    if depth == 0 {
1177                        return Err(Error::schema(format!(
1178                            "Invalid nested type syntax: {}",
1179                            type_str
1180                        )));
1181                    }
1182                    depth -= 1;
1183                }
1184                ',' if depth == 0 => {
1185                    parts.push(type_str[start..index].trim());
1186                    start = index + ch.len_utf8();
1187                }
1188                _ => {}
1189            }
1190        }
1191
1192        if depth != 0 {
1193            return Err(Error::schema(format!(
1194                "Unbalanced nested type syntax: {}",
1195                type_str
1196            )));
1197        }
1198
1199        parts.push(type_str[start..].trim());
1200        Ok(parts.into_iter().filter(|part| !part.is_empty()).collect())
1201    }
1202
1203    /// Parse CQL type string into structured type
1204    pub fn parse(type_str: &str) -> Result<Self> {
1205        let type_str = type_str.trim();
1206
1207        // CQL type keywords are case-insensitive (`SET<TEXT>` == `set<text>`),
1208        // so match collection/frozen/tuple prefixes case-insensitively. Matching
1209        // only lowercase here previously left uppercase collections to fall
1210        // through to a bare `Custom("SET<TEXT>")`, which both broke type-aware
1211        // handling and confused UDT-reference validation (roborev job 51).
1212        fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
1213            s.get(..prefix.len())
1214                .filter(|head| head.eq_ignore_ascii_case(prefix))
1215                .map(|_| &s[prefix.len()..])
1216        }
1217
1218        // Handle frozen types
1219        if let Some(inner) = strip_prefix_ci(type_str, "frozen<") {
1220            if let Some(inner) = inner.strip_suffix('>') {
1221                return Ok(CqlType::Frozen(Box::new(Self::parse(inner)?)));
1222            }
1223        }
1224
1225        // Handle collection types
1226        if let Some(inner) = strip_prefix_ci(type_str, "list<") {
1227            if let Some(inner) = inner.strip_suffix('>') {
1228                return Ok(CqlType::List(Box::new(Self::parse(inner)?)));
1229            }
1230        }
1231
1232        if let Some(inner) = strip_prefix_ci(type_str, "set<") {
1233            if let Some(inner) = inner.strip_suffix('>') {
1234                return Ok(CqlType::Set(Box::new(Self::parse(inner)?)));
1235            }
1236        }
1237
1238        if let Some(inner) = strip_prefix_ci(type_str, "map<") {
1239            if let Some(inner) = inner.strip_suffix('>') {
1240                let parts = Self::split_top_level_types(inner)?;
1241                if parts.len() != 2 {
1242                    return Err(Error::schema(format!("Invalid map type: {}", type_str)));
1243                }
1244                return Ok(CqlType::Map(
1245                    Box::new(Self::parse(parts[0].trim())?),
1246                    Box::new(Self::parse(parts[1].trim())?),
1247                ));
1248            }
1249        }
1250
1251        // Handle tuple types
1252        if let Some(inner) = strip_prefix_ci(type_str, "tuple<") {
1253            if let Some(inner) = inner.strip_suffix('>') {
1254                let parts = Self::split_top_level_types(inner)?;
1255                let mut types = Vec::new();
1256                for part in parts {
1257                    types.push(Self::parse(part.trim())?);
1258                }
1259                return Ok(CqlType::Tuple(types));
1260            }
1261        }
1262
1263        // Handle UDT types - format: udt_name or keyspace.udt_name
1264        // But first check if it's not a primitive type in uppercase
1265        let lowercase_type = type_str.to_lowercase();
1266        let is_primitive = matches!(
1267            lowercase_type.as_str(),
1268            "boolean"
1269                | "bool"
1270                | "tinyint"
1271                | "smallint"
1272                | "int"
1273                | "integer"
1274                | "bigint"
1275                | "long"
1276                | "counter"
1277                | "float"
1278                | "double"
1279                | "decimal"
1280                | "text"
1281                | "varchar"
1282                | "ascii"
1283                | "blob"
1284                | "timestamp"
1285                | "date"
1286                | "time"
1287                | "uuid"
1288                | "timeuuid"
1289                | "inet"
1290                | "duration"
1291        );
1292
1293        if !is_primitive
1294            && type_str
1295                .chars()
1296                .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
1297            && !type_str.chars().all(|c| c.is_ascii_lowercase())
1298        {
1299            // This might be a UDT name - store as custom type for now
1300            // Full validation requires UDT registry context
1301            return Ok(CqlType::Custom(format!("udt:{}", type_str)));
1302        }
1303
1304        // Primitive types
1305        match type_str.to_lowercase().as_str() {
1306            "boolean" | "bool" => Ok(CqlType::Boolean),
1307            "tinyint" => Ok(CqlType::TinyInt),
1308            "smallint" => Ok(CqlType::SmallInt),
1309            "int" | "integer" => Ok(CqlType::Int),
1310            "bigint" | "long" => Ok(CqlType::BigInt),
1311            "counter" => Ok(CqlType::Counter),
1312            "float" => Ok(CqlType::Float),
1313            "double" => Ok(CqlType::Double),
1314            "decimal" => Ok(CqlType::Decimal),
1315            "text" | "varchar" => Ok(CqlType::Text),
1316            "ascii" => Ok(CqlType::Ascii),
1317            "blob" => Ok(CqlType::Blob),
1318            "timestamp" => Ok(CqlType::Timestamp),
1319            "date" => Ok(CqlType::Date),
1320            "time" => Ok(CqlType::Time),
1321            "uuid" => Ok(CqlType::Uuid),
1322            "timeuuid" => Ok(CqlType::TimeUuid),
1323            "inet" => Ok(CqlType::Inet),
1324            "duration" => Ok(CqlType::Duration),
1325            "varint" => Ok(CqlType::Varint),
1326            _ => Ok(CqlType::Custom(type_str.to_string())),
1327        }
1328    }
1329
1330    /// Get the expected byte size for fixed-size types
1331    pub fn fixed_size(&self) -> Option<usize> {
1332        match self {
1333            CqlType::Boolean => Some(1),
1334            CqlType::TinyInt => Some(1),
1335            CqlType::SmallInt => Some(2),
1336            CqlType::Int => Some(4),
1337            CqlType::BigInt => Some(8),
1338            CqlType::Counter => Some(8),
1339            CqlType::Float => Some(4),
1340            CqlType::Double => Some(8),
1341            CqlType::Timestamp => Some(8),
1342            CqlType::Date => Some(4),
1343            CqlType::Time => Some(8),
1344            CqlType::Uuid | CqlType::TimeUuid => Some(16),
1345            CqlType::Inet => Some(16), // IPv6, IPv4 is variable
1346            // Variable size types
1347            CqlType::Text
1348            | CqlType::Ascii
1349            | CqlType::Varchar
1350            | CqlType::Blob
1351            | CqlType::Decimal
1352            | CqlType::Duration
1353            | CqlType::Varint => None,
1354            // Collections and complex types are variable
1355            CqlType::List(_)
1356            | CqlType::Set(_)
1357            | CqlType::Map(_, _)
1358            | CqlType::Tuple(_)
1359            | CqlType::Udt(_, _) => None,
1360            CqlType::Frozen(inner) => inner.fixed_size(),
1361            CqlType::Custom(_) => None,
1362        }
1363    }
1364
1365    /// Check if this type is a collection
1366    pub fn is_collection(&self) -> bool {
1367        matches!(
1368            self,
1369            CqlType::List(_) | CqlType::Set(_) | CqlType::Map(_, _)
1370        )
1371    }
1372}
1373
1374/// Schema management service for handling table schemas and UDT definitions
1375#[derive(Debug)]
1376pub struct SchemaManager {
1377    #[allow(dead_code)]
1378    storage: Arc<StorageEngine>,
1379    schemas: Arc<RwLock<HashMap<String, TableSchema>>>,
1380    /// UDT registry for managing User Defined Types (internal, use accessor methods)
1381    pub(crate) udt_registry: Arc<RwLock<UdtRegistry>>,
1382}
1383
1384impl SchemaManager {
1385    /// Create a new schema manager from a path
1386    pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
1387        // Create temporary storage engine (not actually used in this context)
1388        let config = Config::default();
1389        let platform = Arc::new(crate::platform::Platform::new(&config).await?);
1390        let storage = Arc::new(
1391            StorageEngine::open(
1392                path.as_ref(),
1393                &config,
1394                platform,
1395                #[cfg(feature = "state_machine")]
1396                None,
1397            )
1398            .await?,
1399        );
1400
1401        Ok(Self {
1402            storage,
1403            schemas: Arc::new(RwLock::new(HashMap::new())),
1404            udt_registry: Arc::new(RwLock::new(UdtRegistry::new())),
1405        })
1406    }
1407
1408    /// Create a new schema manager with storage
1409    pub async fn new_with_storage(storage: Arc<StorageEngine>, _config: &Config) -> Result<Self> {
1410        let manager = Self {
1411            storage,
1412            schemas: Arc::new(RwLock::new(HashMap::new())),
1413            udt_registry: Arc::new(RwLock::new(UdtRegistry::new())),
1414        };
1415
1416        // Load built-in UDT definitions for Cassandra 5.0 compatibility
1417        manager.load_default_udts().await;
1418
1419        Ok(manager)
1420    }
1421
1422    /// Create a new schema manager with a pre-loaded SchemaRegistry
1423    ///
1424    /// This constructor is used when schemas are loaded from external .cql files
1425    /// during ingestion, allowing the pre-loaded schemas to be used by the query engine.
1426    ///
1427    /// # Arguments
1428    ///
1429    /// * `storage` - The storage engine instance
1430    /// * `registry` - Pre-loaded schema registry from ingestion
1431    /// * `_config` - Database configuration (currently unused)
1432    pub async fn new_with_registry(
1433        storage: Arc<StorageEngine>,
1434        registry: Arc<tokio::sync::RwLock<registry::SchemaRegistry>>,
1435        _config: &Config,
1436    ) -> Result<Self> {
1437        // Acquire both schemas and UDT registry in a single lock scope to prevent deadlocks
1438        let (loaded_schemas, udt_registry) = {
1439            let registry_guard = registry.read().await;
1440            let schemas = registry_guard.list_schemas(None).await?;
1441            let udt_reg = registry_guard.get_udt_registry();
1442            (schemas, udt_reg)
1443        }; // Lock is dropped here before further processing
1444
1445        // Populate internal schemas map
1446        let mut schemas_map = HashMap::new();
1447        for schema in loaded_schemas {
1448            let table_id = format!("{}.{}", schema.keyspace, schema.table);
1449            schemas_map.insert(table_id, schema);
1450        }
1451
1452        let manager = Self {
1453            storage,
1454            schemas: Arc::new(RwLock::new(schemas_map)),
1455            udt_registry,
1456        };
1457
1458        Ok(manager)
1459    }
1460
1461    /// Load default UDT definitions that are commonly used in Cassandra
1462    async fn load_default_udts(&self) {
1463        // Common address UDT used in many Cassandra schemas
1464        let address_udt = UdtTypeDef::new("test_keyspace".to_string(), "address".to_string())
1465            .with_field("street".to_string(), CqlType::Text, true)
1466            .with_field("city".to_string(), CqlType::Text, true)
1467            .with_field("state".to_string(), CqlType::Text, true)
1468            .with_field("zip_code".to_string(), CqlType::Text, true)
1469            .with_field("country".to_string(), CqlType::Text, true);
1470
1471        self.udt_registry.write().await.register_udt(address_udt);
1472
1473        // Enhanced person UDT with nested address
1474        let person_udt = UdtTypeDef::new("test_keyspace".to_string(), "person".to_string())
1475            .with_field("name".to_string(), CqlType::Text, true)
1476            .with_field("age".to_string(), CqlType::Int, true)
1477            .with_field("email".to_string(), CqlType::Text, true)
1478            .with_field(
1479                "addresses".to_string(),
1480                CqlType::List(Box::new(CqlType::Udt(
1481                    "address".to_string(),
1482                    vec![
1483                        ("street".to_string(), CqlType::Text),
1484                        ("city".to_string(), CqlType::Text),
1485                        ("state".to_string(), CqlType::Text),
1486                        ("zip_code".to_string(), CqlType::Text),
1487                        ("country".to_string(), CqlType::Text),
1488                    ],
1489                ))),
1490                true,
1491            )
1492            .with_field(
1493                "contact_info".to_string(),
1494                CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::Text)),
1495                true,
1496            );
1497
1498        self.udt_registry.write().await.register_udt(person_udt);
1499
1500        // Company UDT with nested person and address relationships
1501        let company_udt = UdtTypeDef::new("test_keyspace".to_string(), "company".to_string())
1502            .with_field("name".to_string(), CqlType::Text, false)
1503            .with_field(
1504                "headquarters".to_string(),
1505                CqlType::Udt(
1506                    "address".to_string(),
1507                    vec![
1508                        ("street".to_string(), CqlType::Text),
1509                        ("city".to_string(), CqlType::Text),
1510                        ("state".to_string(), CqlType::Text),
1511                        ("zip_code".to_string(), CqlType::Text),
1512                        ("country".to_string(), CqlType::Text),
1513                    ],
1514                ),
1515                true,
1516            )
1517            .with_field(
1518                "employees".to_string(),
1519                CqlType::Set(Box::new(CqlType::Udt("person".to_string(), vec![]))),
1520                true,
1521            )
1522            .with_field("founded_year".to_string(), CqlType::Int, true);
1523
1524        self.udt_registry.write().await.register_udt(company_udt);
1525    }
1526
1527    /// Register a new UDT type definition
1528    pub async fn register_udt(&self, udt_def: UdtTypeDef) {
1529        self.udt_registry.write().await.register_udt(udt_def);
1530    }
1531
1532    /// Get a UDT definition (returns a cloned UdtTypeDef)
1533    pub async fn get_udt(&self, keyspace: &str, name: &str) -> Option<UdtTypeDef> {
1534        self.udt_registry
1535            .read()
1536            .await
1537            .get_udt(keyspace, name)
1538            .cloned()
1539    }
1540
1541    /// Load schema for a table
1542    pub async fn load_schema(&self, table_name: &str) -> Result<TableSchema> {
1543        // Read lock first to check if schema exists
1544        let schemas = self.schemas.read().await;
1545        if let Some(schema) = schemas.get(table_name) {
1546            return Ok(schema.clone());
1547        }
1548        drop(schemas); // Explicit drop before write lock
1549
1550        // Create default schema
1551        let schema = self.create_default_schema(table_name);
1552
1553        // Write lock to insert
1554        self.schemas
1555            .write()
1556            .await
1557            .insert(table_name.to_string(), schema.clone());
1558        Ok(schema)
1559    }
1560
1561    /// Create a default schema for unknown tables
1562    fn create_default_schema(&self, table_name: &str) -> TableSchema {
1563        TableSchema {
1564            keyspace: "default".to_string(),
1565            table: table_name.to_string(),
1566            partition_keys: vec![KeyColumn {
1567                name: "id".to_string(),
1568                data_type: "uuid".to_string(),
1569                position: 0,
1570            }],
1571            clustering_keys: vec![],
1572            columns: vec![Column {
1573                name: "id".to_string(),
1574                data_type: "uuid".to_string(),
1575                nullable: false,
1576                default: None,
1577                is_static: false,
1578            }],
1579            comments: HashMap::new(),
1580            dropped_columns: HashMap::new(),
1581        }
1582    }
1583
1584    /// Parse and register a schema from a CQL CREATE TABLE statement
1585    pub async fn parse_and_register_cql_schema(&self, cql: &str) -> Result<TableSchema> {
1586        let schema = cql_parser::parse_cql_schema(cql)?;
1587        let table_key = format!("{}.{}", schema.keyspace, schema.table);
1588        self.schemas
1589            .write()
1590            .await
1591            .insert(table_key.clone(), schema.clone());
1592        Ok(schema)
1593    }
1594
1595    /// Find schema by table name with optional keyspace matching
1596    pub async fn find_schema_by_table(
1597        &self,
1598        keyspace: &Option<String>,
1599        table: &str,
1600    ) -> Option<TableSchema> {
1601        let schemas = self.schemas.read().await;
1602
1603        // First try exact match if keyspace provided
1604        if let Some(ks) = keyspace {
1605            let key = format!("{}.{}", ks, table);
1606            if let Some(schema) = schemas.get(&key) {
1607                return Some(schema.clone());
1608            }
1609        }
1610
1611        // Then try to find any schema matching the table name
1612        schemas
1613            .values()
1614            .find(|schema| {
1615                cql_parser::table_name_matches(
1616                    &Some(schema.keyspace.clone()),
1617                    &schema.table,
1618                    keyspace,
1619                    table,
1620                )
1621            })
1622            .cloned()
1623    }
1624
1625    /// Extract table information from CQL without full parsing
1626    pub fn extract_table_info(&self, cql: &str) -> Result<(Option<String>, String)> {
1627        cql_parser::extract_table_name(cql)
1628    }
1629
1630    /// Convert CQL type string to internal type ID
1631    pub fn cql_type_to_internal(&self, cql_type: &str) -> Result<CqlTypeId> {
1632        cql_parser::cql_type_to_type_id(cql_type)
1633    }
1634
1635    /// Get table schema by name (async for compatibility)
1636    pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema> {
1637        // Try to find schema by table name
1638        if let Some(schema) = self.find_schema_by_table(&None, table_name).await {
1639            Ok(schema)
1640        } else {
1641            Err(Error::Schema(format!(
1642                "Table schema not found: {}",
1643                table_name
1644            )))
1645        }
1646    }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651    use super::*;
1652
1653    #[test]
1654    fn test_schema_validation() {
1655        let schema_json = r#"
1656        {
1657            "keyspace": "test",
1658            "table": "users",
1659            "partition_keys": [
1660                {"name": "id", "type": "bigint", "position": 0}
1661            ],
1662            "clustering_keys": [],
1663            "columns": [
1664                {"name": "id", "type": "bigint", "nullable": false},
1665                {"name": "name", "type": "text", "nullable": true}
1666            ]
1667        }
1668        "#;
1669
1670        let schema = TableSchema::from_json(schema_json).unwrap();
1671        assert_eq!(schema.keyspace, "test");
1672        assert_eq!(schema.table, "users");
1673        assert_eq!(schema.partition_keys.len(), 1);
1674        assert_eq!(schema.columns.len(), 2);
1675    }
1676
1677    #[test]
1678    fn test_cql_type_parsing() {
1679        assert_eq!(CqlType::parse("text").unwrap(), CqlType::Text);
1680        assert_eq!(CqlType::parse("bigint").unwrap(), CqlType::BigInt);
1681
1682        match CqlType::parse("list<int>").unwrap() {
1683            CqlType::List(inner) => assert_eq!(*inner, CqlType::Int),
1684            _ => panic!("Expected List type"),
1685        }
1686
1687        match CqlType::parse("map<text, bigint>").unwrap() {
1688            CqlType::Map(key, value) => {
1689                assert_eq!(*key, CqlType::Text);
1690                assert_eq!(*value, CqlType::BigInt);
1691            }
1692            _ => panic!("Expected Map type"),
1693        }
1694
1695        match CqlType::parse("tuple<text, list<int>, map<text, text>>").unwrap() {
1696            CqlType::Tuple(fields) => {
1697                assert_eq!(fields.len(), 3);
1698                assert_eq!(fields[0], CqlType::Text);
1699                assert_eq!(fields[1], CqlType::List(Box::new(CqlType::Int)));
1700                assert_eq!(
1701                    fields[2],
1702                    CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::Text))
1703                );
1704            }
1705            _ => panic!("Expected Tuple type"),
1706        }
1707    }
1708
1709    #[test]
1710    fn test_schema_validation_failures() {
1711        // Missing partition key
1712        let invalid_schema = r#"
1713        {
1714            "keyspace": "test",
1715            "table": "users", 
1716            "partition_keys": [],
1717            "clustering_keys": [],
1718            "columns": []
1719        }
1720        "#;
1721
1722        assert!(TableSchema::from_json(invalid_schema).is_err());
1723
1724        // Invalid type
1725        let invalid_type = r#"
1726        {
1727            "keyspace": "test",
1728            "table": "users",
1729            "partition_keys": [
1730                {"name": "id", "type": "invalid_type", "position": 0}
1731            ],
1732            "clustering_keys": [],
1733            "columns": [
1734                {"name": "id", "type": "invalid_type", "nullable": false}
1735            ]
1736        }
1737        "#;
1738
1739        // This should succeed as we allow custom types
1740        assert!(TableSchema::from_json(invalid_type).is_ok());
1741    }
1742
1743    fn udt_schema(column_type: &str) -> TableSchema {
1744        TableSchema {
1745            keyspace: "test_ks".to_string(),
1746            table: "t".to_string(),
1747            partition_keys: vec![KeyColumn {
1748                name: "id".to_string(),
1749                data_type: "uuid".to_string(),
1750                position: 0,
1751            }],
1752            clustering_keys: vec![],
1753            columns: vec![
1754                Column {
1755                    name: "id".to_string(),
1756                    data_type: "uuid".to_string(),
1757                    nullable: false,
1758                    default: None,
1759                    is_static: false,
1760                },
1761                Column {
1762                    name: "value".to_string(),
1763                    data_type: column_type.to_string(),
1764                    nullable: true,
1765                    default: None,
1766                    is_static: false,
1767                },
1768            ],
1769            comments: HashMap::new(),
1770            dropped_columns: HashMap::new(),
1771        }
1772    }
1773
1774    #[test]
1775    fn test_udt_reference_undefined_top_level_errors() {
1776        let registry = UdtRegistry::new();
1777        let schema = udt_schema("MyMissingType");
1778
1779        let err = schema
1780            .validate_udt_references(&registry)
1781            .expect_err("undefined UDT reference must fail validation");
1782        let msg = err.to_string();
1783        assert!(
1784            matches!(err, Error::Schema(_)),
1785            "expected schema-category error, got {err:?}"
1786        );
1787        assert!(
1788            msg.contains("MyMissingType"),
1789            "error must name the missing UDT, got: {msg}"
1790        );
1791    }
1792
1793    #[test]
1794    fn test_udt_reference_undefined_lowercase_errors() {
1795        // Regression (roborev job 39): a UDT name that is purely lowercase
1796        // letters (no underscore/digit) parses to a bare `Custom("<name>")`
1797        // with no `udt:` prefix, so validation must still catch it —
1798        // top-level and nested.
1799        let registry = UdtRegistry::new();
1800        for col_type in ["address", "list<frozen<address>>"] {
1801            let schema = udt_schema(col_type);
1802            let err = schema
1803                .validate_udt_references(&registry)
1804                .expect_err("undefined lowercase UDT must fail validation");
1805            assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1806            assert!(
1807                err.to_string().contains("address"),
1808                "error must name the missing UDT, got: {err}"
1809            );
1810        }
1811    }
1812
1813    #[test]
1814    fn test_uppercase_collection_of_primitives_is_not_a_udt() {
1815        // Regression (collections fixture): uppercase collections of primitives
1816        // must parse and not be mistaken for a UDT reference.
1817        let registry = UdtRegistry::new();
1818        for col_type in [
1819            "SET<TEXT>",
1820            "LIST<INT>",
1821            "MAP<TEXT, TEXT>",
1822            "FROZEN<LIST<INT>>",
1823        ] {
1824            let schema = udt_schema(col_type);
1825            schema
1826                .validate_udt_references(&registry)
1827                .unwrap_or_else(|e| panic!("'{col_type}' must not be flagged as a UDT: {e}"));
1828        }
1829    }
1830
1831    #[test]
1832    fn test_uppercase_collection_with_undefined_udt_errors() {
1833        // Regression (roborev job 51): an undefined UDT nested inside an
1834        // UPPERCASE collection must still be detected — case-insensitive parsing
1835        // means the nested reference is validated, not skipped.
1836        let registry = UdtRegistry::new();
1837        for col_type in [
1838            "LIST<MissingType>",
1839            "MAP<TEXT, MissingType>",
1840            "FROZEN<SET<MissingType>>",
1841        ] {
1842            let schema = udt_schema(col_type);
1843            let err = schema
1844                .validate_udt_references(&registry)
1845                .expect_err("undefined UDT in uppercase collection must fail");
1846            assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1847            assert!(
1848                err.to_string().contains("MissingType"),
1849                "error must name the missing UDT, got: {err}"
1850            );
1851        }
1852    }
1853
1854    #[test]
1855    fn test_udt_reference_undefined_nested_in_collection_errors() {
1856        let registry = UdtRegistry::new();
1857        let schema = udt_schema("list<frozen<NestedMissing>>");
1858
1859        let err = schema
1860            .validate_udt_references(&registry)
1861            .expect_err("nested undefined UDT reference must fail validation");
1862        let msg = err.to_string();
1863        assert!(matches!(err, Error::Schema(_)));
1864        assert!(
1865            msg.contains("NestedMissing"),
1866            "error must name the nested missing UDT, got: {msg}"
1867        );
1868    }
1869
1870    #[test]
1871    fn test_udt_reference_undefined_nested_in_map_errors() {
1872        let registry = UdtRegistry::new();
1873        let schema = udt_schema("map<text, MapValueMissing>");
1874
1875        let err = schema
1876            .validate_udt_references(&registry)
1877            .expect_err("undefined UDT in map value must fail validation");
1878        assert!(matches!(err, Error::Schema(_)));
1879        assert!(err.to_string().contains("MapValueMissing"));
1880    }
1881
1882    #[test]
1883    fn test_udt_reference_defined_top_level_ok() {
1884        let mut registry = UdtRegistry::new();
1885        registry.register_udt(
1886            UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1887                "a".to_string(),
1888                CqlType::Text,
1889                true,
1890            ),
1891        );
1892        let schema = udt_schema("MyType");
1893        schema
1894            .validate_udt_references(&registry)
1895            .expect("defined UDT should validate");
1896    }
1897
1898    #[test]
1899    fn test_udt_reference_defined_nested_ok() {
1900        let mut registry = UdtRegistry::new();
1901        registry.register_udt(
1902            UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1903                "a".to_string(),
1904                CqlType::Text,
1905                true,
1906            ),
1907        );
1908        let schema = udt_schema("list<frozen<MyType>>");
1909        schema
1910            .validate_udt_references(&registry)
1911            .expect("defined nested UDT should validate");
1912    }
1913
1914    #[test]
1915    fn test_validate_udt_references_no_udts_ok() {
1916        // Schemas without any UDT columns must validate against an empty registry.
1917        let registry = UdtRegistry::new();
1918        let schema = udt_schema("map<text, list<int>>");
1919        schema
1920            .validate_udt_references(&registry)
1921            .expect("primitive/collection-only schema should validate");
1922    }
1923
1924    #[tokio::test]
1925    async fn test_concurrent_schema_access() {
1926        // Create a SchemaManager for testing concurrent access
1927        let config = Config::default();
1928        let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1929        let temp_dir = tempfile::tempdir().unwrap();
1930        let storage = Arc::new(
1931            StorageEngine::open(
1932                temp_dir.path(),
1933                &config,
1934                platform,
1935                #[cfg(feature = "state_machine")]
1936                None,
1937            )
1938            .await
1939            .unwrap(),
1940        );
1941
1942        let manager = Arc::new(
1943            SchemaManager::new_with_storage(storage, &config)
1944                .await
1945                .unwrap(),
1946        );
1947
1948        // Spawn 10 concurrent tasks accessing 3 different tables
1949        let mut handles = vec![];
1950        for i in 0..10 {
1951            let m = Arc::clone(&manager);
1952            let handle = tokio::spawn(async move {
1953                let table = format!("table_{}", i % 3); // 3 different tables, concurrent access
1954                m.load_schema(&table).await.unwrap()
1955            });
1956            handles.push(handle);
1957        }
1958
1959        // Wait for all tasks to complete
1960        for handle in handles {
1961            handle.await.unwrap();
1962        }
1963
1964        // Verify schemas were created
1965        let schemas = manager.schemas.read().await;
1966        assert!(schemas.len() <= 3); // At most 3 unique tables
1967        assert!(schemas.contains_key("table_0"));
1968        assert!(schemas.contains_key("table_1"));
1969        assert!(schemas.contains_key("table_2"));
1970    }
1971
1972    #[test]
1973    fn test_schema_from_sstable_header() {
1974        use crate::parser::header::{
1975            CassandraVersion, ColumnInfo, CompressionInfo, SSTableHeader, SSTableStats,
1976        };
1977        use std::collections::HashMap;
1978
1979        let columns = vec![
1980            ColumnInfo {
1981                name: "id".to_string(),
1982                column_type: "int".to_string(),
1983                is_primary_key: true,
1984                key_position: Some(0),
1985                is_static: false,
1986                is_clustering: false,
1987                clustering_reversed: false,
1988            },
1989            ColumnInfo {
1990                name: "name".to_string(),
1991                column_type: "text".to_string(),
1992                is_primary_key: false,
1993                key_position: None,
1994                is_static: false,
1995                is_clustering: false,
1996                clustering_reversed: false,
1997            },
1998        ];
1999
2000        let header = SSTableHeader {
2001            cassandra_version: CassandraVersion::V5_0Bti,
2002            version: 1,
2003            table_id: [0; 16],
2004            keyspace: "test_ks".to_string(),
2005            table_name: "test_table".to_string(),
2006            generation: 1,
2007            compression: CompressionInfo {
2008                algorithm: "NONE".to_string(),
2009                chunk_size: 0,
2010                parameters: HashMap::new(),
2011            },
2012            stats: SSTableStats::default(),
2013            columns,
2014            properties: HashMap::new(),
2015        };
2016
2017        let schema = TableSchema::from_sstable_header(&header).unwrap();
2018
2019        assert_eq!(schema.keyspace, "test_ks");
2020        assert_eq!(schema.table, "test_table");
2021        assert_eq!(schema.partition_keys.len(), 1);
2022        assert_eq!(schema.partition_keys[0].name, "id");
2023        assert_eq!(schema.columns.len(), 2);
2024    }
2025}