1pub mod aggregator;
8pub mod cql_parser;
9pub mod discovery;
10#[cfg(feature = "experimental")]
11pub mod json_exporter;
12pub mod parser;
13pub mod registry;
14
15pub use aggregator::{
17 AggregatorConfig, LoadErrorType, LoadResult, SchemaAggregator, SchemaLoadError,
18 SchemaLoadWarning,
19};
20
21pub 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
27pub 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
49pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TableSchema {
68 pub keyspace: String,
70
71 pub table: String,
73
74 pub partition_keys: Vec<KeyColumn>,
76
77 pub clustering_keys: Vec<ClusteringColumn>,
79
80 pub columns: Vec<Column>,
82
83 #[serde(default)]
85 pub comments: HashMap<String, String>,
86
87 #[serde(default)]
111 pub dropped_columns: HashMap<String, i64>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct KeyColumn {
117 pub name: String,
119
120 #[serde(rename = "type")]
122 pub data_type: String,
123
124 pub position: usize,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ClusteringColumn {
131 pub name: String,
133
134 #[serde(rename = "type")]
136 pub data_type: String,
137
138 pub position: usize,
140
141 #[serde(default)]
143 pub order: ClusteringOrder,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
148pub enum ClusteringOrder {
149 #[default]
151 Asc,
152 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#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct Column {
177 pub name: String,
179
180 #[serde(rename = "type")]
182 pub data_type: String,
183
184 #[serde(default)]
186 pub nullable: bool,
187
188 #[serde(default)]
190 pub default: Option<serde_json::Value>,
191
192 #[serde(default)]
194 pub is_static: bool,
195}
196
197#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
199pub enum CqlType {
200 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 List(Box<CqlType>),
225 Set(Box<CqlType>),
226 Map(Box<CqlType>, Box<CqlType>),
227
228 Tuple(Vec<CqlType>),
230 Udt(String, Vec<(String, CqlType)>), Frozen(Box<CqlType>),
232
233 Custom(String),
235}
236
237#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239pub struct UdtRegistry {
240 udts: HashMap<String, HashMap<String, UdtTypeDef>>,
242}
243
244impl UdtRegistry {
245 pub fn new() -> Self {
247 Self {
248 udts: HashMap::new(),
249 }
250 }
251
252 pub fn with_cassandra5_defaults() -> Self {
254 let mut registry = Self::new();
255 registry.load_cassandra5_system_udts();
256 registry
257 }
258
259 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 pub fn get_udt(&self, keyspace: &str, name: &str) -> Option<&UdtTypeDef> {
267 self.udts.get(keyspace)?.get(name)
268 }
269
270 pub fn get_keyspace_udts(&self, keyspace: &str) -> Option<&HashMap<String, UdtTypeDef>> {
272 self.udts.get(keyspace)
273 }
274
275 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 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 pub fn remove_udt(&mut self, keyspace: &str, name: &str) -> Option<UdtTypeDef> {
293 self.udts.get_mut(keyspace)?.remove(name)
294 }
295
296 pub fn clear_keyspace(&mut self, keyspace: &str) {
298 self.udts.remove(keyspace);
299 }
300
301 pub fn total_udts(&self) -> usize {
303 self.udts.values().map(|udts| udts.len()).sum()
304 }
305
306 fn load_cassandra5_system_udts(&mut self) {
308 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 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 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 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 for field in &udt.fields {
387 self.validate_field_type_dependencies(&field.field_type, keyspace)?;
388 }
389
390 Ok(udt)
391 }
392
393 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 _ => {} }
422 Ok(())
423 }
424
425 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; }
434
435 if self.udt_depends_on(udt, udt_name) {
437 dependents.push(udt);
438 }
439 }
440 }
441
442 dependents
443 }
444
445 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 #[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 pub fn register_udt_with_validation(&mut self, udt_def: UdtTypeDef) -> crate::Result<()> {
476 for field in &udt_def.fields {
478 self.validate_field_type_dependencies(&field.field_type, &udt_def.keyspace)?;
479 }
480
481 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 fn would_create_circular_dependency(&self, udt_def: &UdtTypeDef) -> bool {
495 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 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 #[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
574pub(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 pub fn from_sstable_header(header: &SSTableHeader) -> Result<Self> {
594 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 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 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 partition_keys.sort_by_key(|c| c.key_position.unwrap());
633 clustering_keys.sort_by_key(|c| c.key_position.unwrap());
634
635 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, })
645 .collect();
646
647 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, order: if col.clustering_reversed {
661 ClusteringOrder::Desc
662 } else {
663 ClusteringOrder::Asc
664 },
665 })
666 .collect();
667
668 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, default: None,
677 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 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 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 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 pub fn validate(&self) -> Result<()> {
734 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 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 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 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 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 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 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 !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 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 pub fn validate_udt_references(&self, registry: &UdtRegistry) -> Result<()> {
902 for column in &self.columns {
903 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 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::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 _ => {} }
952 Ok(())
953 }
954
955 fn ensure_udt_exists(
958 &self,
959 udt_name: &str,
960 column_name: &str,
961 registry: &UdtRegistry,
962 ) -> Result<()> {
963 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 pub fn get_column(&self, name: &str) -> Option<&Column> {
984 self.columns.iter().find(|c| c.name == name)
985 }
986
987 pub fn is_partition_key(&self, name: &str) -> bool {
989 self.partition_keys.iter().any(|k| k.name == name)
990 }
991
992 pub fn is_clustering_key(&self, name: &str) -> bool {
994 self.clustering_keys.iter().any(|k| k.name == name)
995 }
996
997 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 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 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 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 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 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 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 #[allow(clippy::only_used_in_recursion)]
1077 fn comparators_are_compatible(&self, left: &ComparatorType, right: &ComparatorType) -> bool {
1078 match (left, right) {
1079 (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 (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 (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 (
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 (ComparatorType::Frozen(l_inner), ComparatorType::Frozen(r_inner)) => {
1130 self.comparators_are_compatible(l_inner, r_inner)
1131 }
1132
1133 (ComparatorType::Custom(l_name), ComparatorType::Custom(r_name)) => l_name == r_name,
1135
1136 _ => false,
1138 }
1139 }
1140
1141 #[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 pub fn parse(type_str: &str) -> Result<Self> {
1205 let type_str = type_str.trim();
1206
1207 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 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 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 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 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 return Ok(CqlType::Custom(format!("udt:{}", type_str)));
1302 }
1303
1304 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 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), CqlType::Text
1348 | CqlType::Ascii
1349 | CqlType::Varchar
1350 | CqlType::Blob
1351 | CqlType::Decimal
1352 | CqlType::Duration
1353 | CqlType::Varint => None,
1354 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 pub fn is_collection(&self) -> bool {
1367 matches!(
1368 self,
1369 CqlType::List(_) | CqlType::Set(_) | CqlType::Map(_, _)
1370 )
1371 }
1372}
1373
1374#[derive(Debug)]
1376pub struct SchemaManager {
1377 #[allow(dead_code)]
1378 storage: Arc<StorageEngine>,
1379 schemas: Arc<RwLock<HashMap<String, TableSchema>>>,
1380 pub(crate) udt_registry: Arc<RwLock<UdtRegistry>>,
1382}
1383
1384impl SchemaManager {
1385 pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
1387 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 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 manager.load_default_udts().await;
1418
1419 Ok(manager)
1420 }
1421
1422 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 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 }; 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 async fn load_default_udts(&self) {
1463 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 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 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 pub async fn register_udt(&self, udt_def: UdtTypeDef) {
1529 self.udt_registry.write().await.register_udt(udt_def);
1530 }
1531
1532 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 pub async fn load_schema(&self, table_name: &str) -> Result<TableSchema> {
1543 let schemas = self.schemas.read().await;
1545 if let Some(schema) = schemas.get(table_name) {
1546 return Ok(schema.clone());
1547 }
1548 drop(schemas); let schema = self.create_default_schema(table_name);
1552
1553 self.schemas
1555 .write()
1556 .await
1557 .insert(table_name.to_string(), schema.clone());
1558 Ok(schema)
1559 }
1560
1561 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 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 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 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 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 pub fn extract_table_info(&self, cql: &str) -> Result<(Option<String>, String)> {
1627 cql_parser::extract_table_name(cql)
1628 }
1629
1630 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 pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema> {
1637 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 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 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 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(®istry)
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 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(®istry)
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 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(®istry)
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 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(®istry)
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(®istry)
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(®istry)
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(®istry)
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(®istry)
1911 .expect("defined nested UDT should validate");
1912 }
1913
1914 #[test]
1915 fn test_validate_udt_references_no_udts_ok() {
1916 let registry = UdtRegistry::new();
1918 let schema = udt_schema("map<text, list<int>>");
1919 schema
1920 .validate_udt_references(®istry)
1921 .expect("primitive/collection-only schema should validate");
1922 }
1923
1924 #[tokio::test]
1925 async fn test_concurrent_schema_access() {
1926 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 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); m.load_schema(&table).await.unwrap()
1955 });
1956 handles.push(handle);
1957 }
1958
1959 for handle in handles {
1961 handle.await.unwrap();
1962 }
1963
1964 let schemas = manager.schemas.read().await;
1966 assert!(schemas.len() <= 3); 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}