1use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::SystemTime;
11
12use serde::{Deserialize, Serialize};
13use tokio::sync::RwLock;
14
15use crate::{
16 platform::Platform,
17 schema::{
18 discovery::{SchemaDiscoveryConfig, SchemaDiscoveryEngine, SchemaInfo},
19 CqlType, TableSchema, UdtRegistry,
20 },
21 types::{ComparatorType, UdtTypeDef},
22 Config, Error, Result,
23};
24
25#[derive(Debug, Clone)]
27pub struct SchemaRegistryConfig {
28 pub enable_auto_discovery: bool,
30 pub enable_caching: bool,
32 pub cache_ttl_seconds: u64,
34 pub enable_versioning: bool,
36 pub max_versions_per_schema: usize,
38 pub enable_validation: bool,
40 pub auto_refresh_on_changes: bool,
42 pub discovery_config: SchemaDiscoveryConfig,
44}
45
46impl Default for SchemaRegistryConfig {
47 fn default() -> Self {
48 Self {
49 enable_auto_discovery: true,
50 enable_caching: true,
51 cache_ttl_seconds: 3600, enable_versioning: true,
53 max_versions_per_schema: 5,
54 enable_validation: true,
55 auto_refresh_on_changes: false, discovery_config: SchemaDiscoveryConfig::default(),
57 }
58 }
59}
60
61#[derive(Debug)]
63pub struct SchemaRegistry {
64 config: SchemaRegistryConfig,
66 _platform: Arc<Platform>,
68 _core_config: Config,
70 schemas: Arc<RwLock<HashMap<String, SchemaEntry>>>,
72 udt_registry: Arc<RwLock<UdtRegistry>>,
74 discovery_engine: Arc<SchemaDiscoveryEngine>,
76 validator: Arc<SchemaValidator>,
78 version_history: Arc<RwLock<HashMap<String, Vec<SchemaVersion>>>>,
80 update_locks: std::sync::Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
96}
97
98#[derive(Debug, Clone)]
108struct DerivedParsingState {
109 schema: Arc<TableSchema>,
111 partition_comparators: Arc<Vec<ComparatorType>>,
113 clustering_comparators: Arc<Vec<ComparatorType>>,
115 column_comparators: Arc<HashMap<String, ComparatorType>>,
117}
118
119impl DerivedParsingState {
120 fn compute(schema: &TableSchema) -> Result<Self> {
123 let mut partition_comparators = Vec::new();
124 for key_column in schema.ordered_partition_keys() {
125 let cql_type = CqlType::parse(&key_column.data_type)?;
126 partition_comparators.push(ComparatorType::from_cql_type(&cql_type)?);
127 }
128
129 let mut clustering_comparators = Vec::new();
130 for key_column in schema.ordered_clustering_keys() {
131 let cql_type = CqlType::parse(&key_column.data_type)?;
132 clustering_comparators.push(ComparatorType::from_cql_type(&cql_type)?);
133 }
134
135 let mut column_comparators = HashMap::new();
136 for column in &schema.columns {
137 let cql_type = CqlType::parse(&column.data_type)?;
138 column_comparators.insert(
139 column.name.clone(),
140 ComparatorType::from_cql_type(&cql_type)?,
141 );
142 }
143
144 Ok(Self {
145 schema: Arc::new(schema.clone()),
146 partition_comparators: Arc::new(partition_comparators),
147 clustering_comparators: Arc::new(clustering_comparators),
148 column_comparators: Arc::new(column_comparators),
149 })
150 }
151
152 fn to_parsing_context(&self) -> ParsingContext {
155 ParsingContext {
156 schema: self.schema.clone(),
157 partition_comparators: self.partition_comparators.clone(),
158 clustering_comparators: self.clustering_comparators.clone(),
159 column_comparators: self.column_comparators.clone(),
160 }
161 }
162}
163
164#[derive(Debug, Clone)]
166struct SchemaEntry {
167 schema: TableSchema,
169 derived: Option<DerivedParsingState>,
174 extended_info: Option<SchemaInfo>,
176 registered_at: SystemTime,
178 source: SchemaSource,
180 validation_status: SchemaValidationStatus,
182 _associated_files: Vec<PathBuf>,
184}
185
186#[derive(Debug, Clone)]
188pub enum SchemaSource {
189 Discovered(Vec<PathBuf>),
191 External(PathBuf),
193 Cql(String),
195 Manual,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum SchemaValidationStatus {
202 Valid,
204 ValidWithWarnings,
206 Invalid,
208 NotValidated,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct SchemaVersion {
215 pub version: u32,
217 pub created_at: SystemTime,
219 pub schema: TableSchema,
221 pub changes: Vec<SchemaChange>,
223 pub source: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct SchemaChange {
230 pub change_type: SchemaChangeType,
232 pub component: String,
234 pub description: String,
236 pub old_value: Option<String>,
238 pub new_value: Option<String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
244pub enum SchemaChangeType {
245 ColumnAdded,
247 ColumnRemoved,
249 ColumnTypeChanged,
251 ColumnRenamed,
253 IndexAdded,
255 IndexRemoved,
257 UdtAdded,
259 UdtModified,
261 UdtRemoved,
263 TableOptionChanged,
265}
266
267#[derive(Debug, Clone)]
269pub struct ValidationReport {
270 pub table_id: String,
272 pub status: SchemaValidationStatus,
274 pub errors: Vec<ValidationError>,
276 pub warnings: Vec<ValidationWarning>,
278 pub recommendations: Vec<String>,
280 pub validated_at: SystemTime,
282}
283
284#[derive(Debug, Clone)]
286pub struct ValidationError {
287 pub code: String,
289 pub message: String,
291 pub component: Option<String>,
293 pub severity: ErrorSeverity,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum ErrorSeverity {
300 Critical,
301 High,
302 Medium,
303 Low,
304}
305
306#[derive(Debug, Clone)]
308pub struct ValidationWarning {
309 pub code: String,
311 pub message: String,
313 pub component: Option<String>,
315}
316
317#[derive(Debug, Clone)]
319pub struct SchemaQuery {
320 pub keyspace: Option<String>,
322 pub table_pattern: Option<String>,
324 pub source_types: Option<Vec<SchemaSource>>,
326 pub validated_only: bool,
328 pub include_history: bool,
330}
331
332impl SchemaRegistry {
333 pub async fn new(
335 config: SchemaRegistryConfig,
336 platform: Arc<Platform>,
337 core_config: Config,
338 ) -> Result<Self> {
339 let discovery_engine = Arc::new(
340 SchemaDiscoveryEngine::new(
341 config.discovery_config.clone(),
342 platform.clone(),
343 core_config.clone(),
344 )
345 .await?,
346 );
347
348 let validator = Arc::new(SchemaValidator::new());
349 let udt_registry = Arc::new(RwLock::new(UdtRegistry::new()));
350
351 Ok(Self {
352 config,
353 _platform: platform,
354 _core_config: core_config,
355 schemas: Arc::new(RwLock::new(HashMap::new())),
356 udt_registry,
357 discovery_engine,
358 validator,
359 version_history: Arc::new(RwLock::new(HashMap::new())),
360 update_locks: std::sync::Mutex::new(HashMap::new()),
361 })
362 }
363
364 fn table_update_lock(&self, table_id: &str) -> Arc<tokio::sync::Mutex<()>> {
371 let mut locks = self
372 .update_locks
373 .lock()
374 .unwrap_or_else(|poisoned| poisoned.into_inner());
375 locks
376 .entry(table_id.to_string())
377 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
378 .clone()
379 }
380
381 pub async fn discover_schema(
383 &self,
384 keyspace: &str,
385 table: &str,
386 sstable_files: &[PathBuf],
387 ) -> Result<TableSchema> {
388 if !self.config.enable_auto_discovery {
389 return Err(Error::Schema("Auto-discovery is disabled".to_string()));
390 }
391
392 let schema_info = self
394 .discovery_engine
395 .discover_schema(keyspace, table, sstable_files)
396 .await?;
397
398 let table_schema = self.convert_schema_info_to_table_schema(&schema_info)?;
400
401 self.register_discovered_schema(
403 table_schema.clone(),
404 Some(schema_info),
405 sstable_files.to_vec(),
406 )
407 .await?;
408
409 Ok(table_schema)
410 }
411
412 pub async fn register_schema(&self, schema: TableSchema, source: SchemaSource) -> Result<()> {
414 let table_id = format!("{}.{}", schema.keyspace, schema.table);
415
416 let table_lock = self.table_update_lock(&table_id);
437 let _update_guard = table_lock.lock().await;
438
439 let validation_status = if self.config.enable_validation {
441 match self.validator.validate_table_schema(&schema).await {
442 Ok(_) => SchemaValidationStatus::Valid,
443 Err(_) => SchemaValidationStatus::Invalid,
444 }
445 } else {
446 SchemaValidationStatus::NotValidated
447 };
448
449 let derived = DerivedParsingState::compute(&schema).ok();
454 let entry = SchemaEntry {
455 schema: schema.clone(),
456 derived,
457 extended_info: None,
458 registered_at: SystemTime::now(),
459 source,
460 validation_status,
461 _associated_files: Vec::new(),
462 };
463
464 let existed = {
480 let mut schemas = self.schemas.write().await;
481 let existed = schemas.contains_key(&table_id);
482 schemas.insert(table_id.clone(), entry);
483 existed
484 }; if self.config.enable_versioning && existed {
487 self.create_schema_version(&table_id, &schema).await?;
488 }
489
490 Ok(())
491 }
492
493 pub async fn get_schema(&self, keyspace: &str, table: &str) -> Result<TableSchema> {
495 let table_id = format!("{}.{}", keyspace, table);
496 let schemas = self.schemas.read().await;
497
498 match schemas.get(&table_id) {
499 Some(entry) => {
500 if self.is_entry_expired(entry) {
502 drop(schemas); return self.refresh_schema(keyspace, table).await;
504 }
505 #[cfg(test)]
506 crate::schema::work_counters::record_schema_clone();
507 Ok(entry.schema.clone())
508 }
509 None => {
510 drop(schemas); if self.config.enable_auto_discovery {
513 self.auto_discover_schema(keyspace, table).await
514 } else {
515 Err(Error::Schema(format!(
516 "Schema not found: {}.{}",
517 keyspace, table
518 )))
519 }
520 }
521 }
522 }
523
524 pub async fn get_schema_info(&self, keyspace: &str, table: &str) -> Result<Option<SchemaInfo>> {
526 let table_id = format!("{}.{}", keyspace, table);
527 let schemas = self.schemas.read().await;
528
529 match schemas.get(&table_id) {
530 Some(entry) => Ok(entry.extended_info.clone()),
531 None => Ok(None),
532 }
533 }
534
535 pub async fn list_schemas(&self, query: Option<SchemaQuery>) -> Result<Vec<TableSchema>> {
537 let schemas = self.schemas.read().await;
538 let mut results = Vec::new();
539
540 for (_table_id, entry) in schemas.iter() {
541 if let Some(ref q) = query {
543 if !self.matches_query(&entry.schema, q) {
544 continue;
545 }
546 }
547
548 results.push(entry.schema.clone());
549 }
550
551 results.sort_by(|a, b| {
553 a.keyspace
554 .cmp(&b.keyspace)
555 .then_with(|| a.table.cmp(&b.table))
556 });
557
558 Ok(results)
559 }
560
561 pub async fn find_schema_by_table(
580 &self,
581 keyspace: &Option<String>,
582 table: &str,
583 ) -> Result<Option<TableSchema>> {
584 enum Matched {
589 Fresh(TableSchema),
590 Expired { keyspace: String, table: String },
591 }
592
593 let matched = {
594 let schemas = self.schemas.read().await;
595
596 let mut hit = keyspace
598 .as_ref()
599 .and_then(|ks| schemas.get(&format!("{}.{}", ks, table)));
600
601 if hit.is_none() {
603 hit = schemas.values().find(|entry| {
604 crate::schema::table_name_matches(
605 &Some(entry.schema.keyspace.clone()),
606 &entry.schema.table,
607 keyspace,
608 table,
609 )
610 });
611 }
612
613 match hit {
614 None => None,
615 Some(entry) if self.is_entry_expired(entry) => Some(Matched::Expired {
616 keyspace: entry.schema.keyspace.clone(),
617 table: entry.schema.table.clone(),
618 }),
619 Some(entry) => Some(Matched::Fresh(entry.schema.clone())),
620 }
621 }; match matched {
624 None => Ok(None),
626 Some(Matched::Fresh(schema)) => Ok(Some(schema)),
627 Some(Matched::Expired { keyspace, table }) => {
631 self.refresh_schema(&keyspace, &table).await.map(Some)
632 }
633 }
634 }
635
636 #[allow(dead_code)]
638 pub async fn validate_schema(&self, keyspace: &str, table: &str) -> Result<ValidationReport> {
639 let schema = self.get_schema(keyspace, table).await?;
640 let table_id = format!("{}.{}", keyspace, table);
641
642 let mut errors = Vec::new();
644 let mut warnings = Vec::new();
645 let mut recommendations = Vec::new();
646
647 if let Err(e) = schema.validate() {
649 errors.push(ValidationError {
650 code: "SCHEMA_INVALID".to_string(),
651 message: e.to_string(),
652 component: None,
653 severity: ErrorSeverity::Critical,
654 });
655 }
656
657 self.validate_schema_udts(&schema, &mut errors, &mut warnings)
659 .await;
660
661 self.validate_column_types(&schema, &mut errors, &mut warnings)
663 .await;
664
665 self.generate_performance_recommendations(&schema, &mut recommendations)
667 .await;
668
669 let status = if !errors.is_empty() {
671 SchemaValidationStatus::Invalid
672 } else if !warnings.is_empty() {
673 SchemaValidationStatus::ValidWithWarnings
674 } else {
675 SchemaValidationStatus::Valid
676 };
677
678 {
680 let mut schemas = self.schemas.write().await;
681 if let Some(entry) = schemas.get_mut(&table_id) {
682 entry.validation_status = status.clone();
683 }
684 }
685
686 Ok(ValidationReport {
687 table_id,
688 status,
689 errors,
690 warnings,
691 recommendations,
692 validated_at: SystemTime::now(),
693 })
694 }
695
696 pub async fn get_schema_history(
698 &self,
699 keyspace: &str,
700 table: &str,
701 ) -> Result<Vec<SchemaVersion>> {
702 if !self.config.enable_versioning {
703 return Err(Error::Schema("Schema versioning is disabled".to_string()));
704 }
705
706 let table_id = format!("{}.{}", keyspace, table);
707 let history = self.version_history.read().await;
708
709 Ok(history.get(&table_id).cloned().unwrap_or_default())
710 }
711
712 pub async fn remove_schema(&self, keyspace: &str, table: &str) -> Result<()> {
714 let table_id = format!("{}.{}", keyspace, table);
715
716 {
717 let mut schemas = self.schemas.write().await;
718 schemas.remove(&table_id);
719 }
720
721 if self.config.enable_versioning {
723 let mut history = self.version_history.write().await;
724 history.remove(&table_id);
725 }
726
727 Ok(())
728 }
729
730 pub async fn generate_cql(&self, keyspace: &str, table: &str) -> Result<String> {
732 if let Some(schema_info) = self.get_schema_info(keyspace, table).await? {
734 return self.discovery_engine.generate_cql(&schema_info).await;
735 }
736
737 let schema = self.get_schema(keyspace, table).await?;
739 Ok(self.generate_basic_cql(&schema))
740 }
741
742 #[cfg(feature = "experimental")]
744 pub async fn export_schema_json(&self, keyspace: &str, table: &str) -> Result<String> {
745 self.export_schema_json_with_config(
746 keyspace,
747 table,
748 &crate::schema::json_exporter::JsonExportConfig::default(),
749 )
750 .await
751 }
752
753 #[cfg(not(feature = "experimental"))]
754 pub async fn export_schema_json(&self, _keyspace: &str, _table: &str) -> Result<String> {
755 Err(crate::error::Error::unsupported_format(
756 "JSON export requires experimental feature",
757 ))
758 }
759
760 #[cfg(feature = "experimental")]
762 pub async fn export_schema_json_with_config(
763 &self,
764 keyspace: &str,
765 table: &str,
766 config: &crate::schema::json_exporter::JsonExportConfig,
767 ) -> Result<String> {
768 if let Some(schema_info) = self.get_schema_info(keyspace, table).await? {
770 return self
771 .discovery_engine
772 .export_json_with_config(&schema_info, config)
773 .await;
774 }
775
776 let schema = self.get_schema(keyspace, table).await?;
778 let exporter = crate::schema::json_exporter::JsonExporter::with_config(config.clone());
779 exporter.export_table_schema(&schema)
780 }
781
782 #[cfg(not(feature = "experimental"))]
783 pub async fn export_schema_json_with_config<T>(
784 &self,
785 _keyspace: &str,
786 _table: &str,
787 _config: &T,
788 ) -> Result<String> {
789 Err(crate::error::Error::unsupported_format(
790 "JSON export requires experimental feature",
791 ))
792 }
793
794 #[cfg(feature = "experimental")]
796 pub async fn export_schema_json_compact(&self, keyspace: &str, table: &str) -> Result<String> {
797 let config = crate::schema::json_exporter::JsonExportConfig {
798 format_variant: crate::schema::json_exporter::JsonFormat::Compact,
799 include_metadata: false,
800 include_performance_metrics: false,
801 include_type_details: false,
802 pretty_format: false,
803 ..Default::default()
804 };
805 self.export_schema_json_with_config(keyspace, table, &config)
806 .await
807 }
808
809 #[cfg(not(feature = "experimental"))]
810 pub async fn export_schema_json_compact(
811 &self,
812 _keyspace: &str,
813 _table: &str,
814 ) -> Result<String> {
815 Err(crate::error::Error::unsupported_format(
816 "JSON export requires experimental feature",
817 ))
818 }
819
820 #[cfg(feature = "experimental")]
822 pub async fn export_schema_json_openapi(&self, keyspace: &str, table: &str) -> Result<String> {
823 let config = crate::schema::json_exporter::JsonExportConfig {
824 format_variant: crate::schema::json_exporter::JsonFormat::OpenApi,
825 include_documentation: true,
826 include_type_details: true,
827 include_metadata: false,
828 ..Default::default()
829 };
830 self.export_schema_json_with_config(keyspace, table, &config)
831 .await
832 }
833
834 #[cfg(not(feature = "experimental"))]
835 pub async fn export_schema_json_openapi(
836 &self,
837 _keyspace: &str,
838 _table: &str,
839 ) -> Result<String> {
840 Err(crate::error::Error::unsupported_format(
841 "JSON export requires experimental feature",
842 ))
843 }
844
845 #[cfg(feature = "experimental")]
847 pub async fn export_schema_json_pipeline(&self, keyspace: &str, table: &str) -> Result<String> {
848 let config = crate::schema::json_exporter::JsonExportConfig {
849 format_variant: crate::schema::json_exporter::JsonFormat::DataPipeline,
850 include_type_details: true,
851 include_table_options: false,
852 include_performance_metrics: true,
853 ..Default::default()
854 };
855 self.export_schema_json_with_config(keyspace, table, &config)
856 .await
857 }
858
859 #[cfg(not(feature = "experimental"))]
860 pub async fn export_schema_json_pipeline(
861 &self,
862 _keyspace: &str,
863 _table: &str,
864 ) -> Result<String> {
865 Err(crate::error::Error::unsupported_format(
866 "JSON export requires experimental feature",
867 ))
868 }
869
870 #[cfg(feature = "experimental")]
872 pub async fn export_multiple_schemas_json(
873 &self,
874 schema_infos: &[SchemaInfo],
875 ) -> Result<String> {
876 let exporter = crate::schema::json_exporter::JsonExporter::new();
877 exporter.export_multiple_schemas(schema_infos)
878 }
879
880 #[cfg(not(feature = "experimental"))]
881 pub async fn export_multiple_schemas_json(
882 &self,
883 _schema_infos: &[SchemaInfo],
884 ) -> Result<String> {
885 Err(crate::error::Error::unsupported_format(
886 "JSON export requires experimental feature",
887 ))
888 }
889
890 #[cfg(feature = "experimental")]
892 pub async fn export_keyspace_schemas_json(&self, keyspace: &str) -> Result<String> {
893 let mut schema_infos = Vec::new();
894
895 for (_table_id, entry) in self.schemas.read().await.iter() {
897 if entry.schema.keyspace == keyspace {
898 if let Ok(Some(schema_info)) = self
900 .get_schema_info(&entry.schema.keyspace, &entry.schema.table)
901 .await
902 {
903 schema_infos.push(schema_info);
904 }
905 }
906 }
907
908 if schema_infos.is_empty() {
909 return Err(Error::NotFound(format!(
910 "No schemas found in keyspace '{}'",
911 keyspace
912 )));
913 }
914
915 self.export_multiple_schemas_json(&schema_infos).await
916 }
917
918 #[cfg(not(feature = "experimental"))]
919 pub async fn export_keyspace_schemas_json(&self, _keyspace: &str) -> Result<String> {
920 Err(crate::error::Error::unsupported_format(
921 "JSON export requires experimental feature",
922 ))
923 }
924
925 pub async fn register_udt(&self, udt_def: UdtTypeDef) -> Result<()> {
927 let mut registry = self.udt_registry.write().await;
928 registry.register_udt(udt_def);
929 Ok(())
930 }
931
932 pub async fn get_udt(&self, keyspace: &str, name: &str) -> Result<Option<UdtTypeDef>> {
934 let registry = self.udt_registry.read().await;
935 Ok(registry.get_udt(keyspace, name).cloned())
936 }
937
938 #[cfg(feature = "state_machine")]
947 pub(crate) fn get_udt_registry(&self) -> Arc<RwLock<UdtRegistry>> {
948 self.udt_registry.clone()
949 }
950
951 pub async fn get_column_comparator(
953 &self,
954 keyspace: &str,
955 table: &str,
956 column: &str,
957 ) -> Result<ComparatorType> {
958 let schema = self.get_schema(keyspace, table).await?;
959
960 let column_def = schema
962 .columns
963 .iter()
964 .find(|c| c.name == column)
965 .ok_or_else(|| {
966 Error::Schema(format!(
967 "Column '{}' not found in table '{}.{}'",
968 column, keyspace, table
969 ))
970 })?;
971
972 let cql_type = CqlType::parse(&column_def.data_type)?;
974 ComparatorType::from_cql_type(&cql_type)
975 }
976
977 pub async fn get_table_comparators(
979 &self,
980 keyspace: &str,
981 table: &str,
982 ) -> Result<HashMap<String, ComparatorType>> {
983 let schema = self.get_schema(keyspace, table).await?;
984 let mut comparators = HashMap::new();
985
986 for column in &schema.columns {
987 let cql_type = CqlType::parse(&column.data_type)?;
988 let comparator = ComparatorType::from_cql_type(&cql_type)?;
989 comparators.insert(column.name.clone(), comparator);
990 }
991
992 Ok(comparators)
993 }
994
995 pub async fn get_partition_key_comparator(
997 &self,
998 keyspace: &str,
999 table: &str,
1000 ) -> Result<Vec<ComparatorType>> {
1001 let schema = self.get_schema(keyspace, table).await?;
1002 let mut comparators = Vec::new();
1003
1004 let ordered_keys = schema.ordered_partition_keys();
1006 for key_column in ordered_keys {
1007 let cql_type = CqlType::parse(&key_column.data_type)?;
1008 let comparator = ComparatorType::from_cql_type(&cql_type)?;
1009 comparators.push(comparator);
1010 }
1011
1012 Ok(comparators)
1013 }
1014
1015 pub async fn get_parsing_context(&self, keyspace: &str, table: &str) -> Result<ParsingContext> {
1022 let table_id = format!("{}.{}", keyspace, table);
1023
1024 {
1027 let schemas = self.schemas.read().await;
1028 if let Some(entry) = schemas.get(&table_id) {
1029 if !self.is_entry_expired(entry) {
1030 match &entry.derived {
1031 Some(derived) => return Ok(derived.to_parsing_context()),
1032 None => {
1035 return DerivedParsingState::compute(&entry.schema)
1036 .map(|d| d.to_parsing_context())
1037 }
1038 }
1039 }
1040 }
1041 }
1042
1043 self.get_schema(keyspace, table).await?;
1047
1048 let schemas = self.schemas.read().await;
1049 match schemas.get(&table_id) {
1050 Some(entry) => match &entry.derived {
1051 Some(derived) => Ok(derived.to_parsing_context()),
1052 None => DerivedParsingState::compute(&entry.schema).map(|d| d.to_parsing_context()),
1053 },
1054 None => Err(Error::Schema(format!(
1055 "Schema not found: {}.{}",
1056 keyspace, table
1057 ))),
1058 }
1059 }
1060
1061 pub async fn get_clustering_key_comparator(
1063 &self,
1064 keyspace: &str,
1065 table: &str,
1066 ) -> Result<Vec<ComparatorType>> {
1067 let schema = self.get_schema(keyspace, table).await?;
1068 let mut comparators = Vec::new();
1069
1070 let ordered_keys = schema.ordered_clustering_keys();
1072 for key_column in ordered_keys {
1073 let cql_type = CqlType::parse(&key_column.data_type)?;
1074 let comparator = ComparatorType::from_cql_type(&cql_type)?;
1075 comparators.push(comparator);
1076 }
1077
1078 Ok(comparators)
1079 }
1080
1081 pub async fn validate_column_type_compatibility(
1083 &self,
1084 keyspace: &str,
1085 table: &str,
1086 column: &str,
1087 expected_type: &str,
1088 ) -> Result<bool> {
1089 let column_comparator = self.get_column_comparator(keyspace, table, column).await?;
1090 let expected_cql_type = CqlType::parse(expected_type)?;
1091 let expected_comparator = ComparatorType::from_cql_type(&expected_cql_type)?;
1092
1093 Ok(self.comparators_are_compatible(&column_comparator, &expected_comparator))
1095 }
1096
1097 #[allow(clippy::only_used_in_recursion)]
1099 fn comparators_are_compatible(&self, left: &ComparatorType, right: &ComparatorType) -> bool {
1100 match (left, right) {
1101 (ComparatorType::Boolean, ComparatorType::Boolean) => true,
1103 (ComparatorType::TinyInt, ComparatorType::TinyInt) => true,
1104 (ComparatorType::SmallInt, ComparatorType::SmallInt) => true,
1105 (ComparatorType::Int, ComparatorType::Int) => true,
1106 (ComparatorType::BigInt, ComparatorType::BigInt) => true,
1107 (ComparatorType::Float32, ComparatorType::Float32) => true,
1108 (ComparatorType::Float, ComparatorType::Float) => true,
1109 (ComparatorType::Text, ComparatorType::Text) => true,
1110 (ComparatorType::Blob, ComparatorType::Blob) => true,
1111 (ComparatorType::Timestamp, ComparatorType::Timestamp) => true,
1112 (ComparatorType::Uuid, ComparatorType::Uuid) => true,
1113 (ComparatorType::Json, ComparatorType::Json) => true,
1114
1115 (ComparatorType::List(l_elem), ComparatorType::List(r_elem)) => {
1117 self.comparators_are_compatible(l_elem, r_elem)
1118 }
1119 (ComparatorType::Set(l_elem), ComparatorType::Set(r_elem)) => {
1120 self.comparators_are_compatible(l_elem, r_elem)
1121 }
1122 (ComparatorType::Map(l_key, l_val), ComparatorType::Map(r_key, r_val)) => {
1123 self.comparators_are_compatible(l_key, r_key)
1124 && self.comparators_are_compatible(l_val, r_val)
1125 }
1126
1127 (ComparatorType::Tuple(l_fields), ComparatorType::Tuple(r_fields)) => {
1129 l_fields.len() == r_fields.len()
1130 && l_fields
1131 .iter()
1132 .zip(r_fields.iter())
1133 .all(|(l, r)| self.comparators_are_compatible(l, r))
1134 }
1135
1136 (
1138 ComparatorType::Udt {
1139 type_name: l_name,
1140 keyspace: l_ks,
1141 ..
1142 },
1143 ComparatorType::Udt {
1144 type_name: r_name,
1145 keyspace: r_ks,
1146 ..
1147 },
1148 ) => l_name == r_name && l_ks == r_ks,
1149
1150 (ComparatorType::Frozen(l_inner), ComparatorType::Frozen(r_inner)) => {
1152 self.comparators_are_compatible(l_inner, r_inner)
1153 }
1154
1155 (ComparatorType::Custom(l_name), ComparatorType::Custom(r_name)) => l_name == r_name,
1157
1158 _ => false,
1160 }
1161 }
1162
1163 pub async fn get_statistics(&self) -> Result<RegistryStatistics> {
1165 let schemas = self.schemas.read().await;
1166 let udt_registry = self.udt_registry.read().await;
1167 let version_history = self.version_history.read().await;
1168
1169 let mut stats = RegistryStatistics {
1170 total_schemas: schemas.len(),
1171 schemas_by_keyspace: HashMap::new(),
1172 validated_schemas: 0,
1173 schemas_with_warnings: 0,
1174 invalid_schemas: 0,
1175 total_udts: udt_registry.total_udts(),
1176 total_versions: version_history.values().map(|v| v.len()).sum(),
1177 auto_discovered_schemas: 0,
1178 manually_registered_schemas: 0,
1179 cache_hit_rate: 0.0, };
1181
1182 for entry in schemas.values() {
1184 let keyspace = &entry.schema.keyspace;
1185 *stats
1186 .schemas_by_keyspace
1187 .entry(keyspace.clone())
1188 .or_insert(0) += 1;
1189
1190 match entry.validation_status {
1191 SchemaValidationStatus::Valid => stats.validated_schemas += 1,
1192 SchemaValidationStatus::ValidWithWarnings => stats.schemas_with_warnings += 1,
1193 SchemaValidationStatus::Invalid => stats.invalid_schemas += 1,
1194 SchemaValidationStatus::NotValidated => {}
1195 }
1196
1197 match entry.source {
1198 SchemaSource::Discovered(_) => stats.auto_discovered_schemas += 1,
1199 _ => stats.manually_registered_schemas += 1,
1200 }
1201 }
1202
1203 Ok(stats)
1204 }
1205
1206 async fn register_discovered_schema(
1209 &self,
1210 schema: TableSchema,
1211 schema_info: Option<SchemaInfo>,
1212 sstable_files: Vec<PathBuf>,
1213 ) -> Result<()> {
1214 let table_id = format!("{}.{}", schema.keyspace, schema.table);
1215 let source = SchemaSource::Discovered(sstable_files.clone());
1216
1217 let derived = DerivedParsingState::compute(&schema).ok();
1220 let entry = SchemaEntry {
1221 schema,
1222 derived,
1223 extended_info: schema_info,
1224 registered_at: SystemTime::now(),
1225 source,
1226 validation_status: SchemaValidationStatus::Valid, _associated_files: sstable_files,
1228 };
1229
1230 let mut schemas = self.schemas.write().await;
1231 schemas.insert(table_id, entry);
1232
1233 Ok(())
1234 }
1235
1236 fn convert_schema_info_to_table_schema(&self, schema_info: &SchemaInfo) -> Result<TableSchema> {
1237 let mut columns = Vec::new();
1238 let mut partition_keys = Vec::new();
1239 let mut clustering_keys = Vec::new();
1240
1241 for (pos, pk) in schema_info.partition_key.iter().enumerate() {
1243 partition_keys.push(crate::schema::KeyColumn {
1244 name: pk.name.clone(),
1245 data_type: pk.data_type.clone(),
1246 position: pos,
1247 });
1248 }
1249
1250 for ck in &schema_info.clustering_keys {
1252 clustering_keys.push(ck.clone());
1253 }
1254
1255 for col in &schema_info.regular_columns {
1257 columns.push(crate::schema::Column {
1258 name: col.name.clone(),
1259 data_type: col.data_type.clone(),
1260 nullable: col.nullable,
1261 default: None, is_static: false,
1263 });
1264 }
1265
1266 for col in &schema_info.static_columns {
1268 columns.push(crate::schema::Column {
1269 name: col.name.clone(),
1270 data_type: col.data_type.clone(),
1271 nullable: col.nullable,
1272 default: None, is_static: true,
1274 });
1275 }
1276
1277 Ok(TableSchema {
1278 keyspace: schema_info.keyspace.clone(),
1279 table: schema_info.table.clone(),
1280 partition_keys,
1281 clustering_keys,
1282 columns,
1283 comments: HashMap::new(),
1284 dropped_columns: HashMap::new(),
1285 })
1286 }
1287
1288 fn is_entry_expired(&self, entry: &SchemaEntry) -> bool {
1289 if !self.config.enable_caching {
1290 return false;
1291 }
1292
1293 let ttl = std::time::Duration::from_secs(self.config.cache_ttl_seconds);
1294 entry
1295 .registered_at
1296 .elapsed()
1297 .unwrap_or(std::time::Duration::ZERO)
1298 > ttl
1299 }
1300
1301 async fn refresh_schema(&self, keyspace: &str, table: &str) -> Result<TableSchema> {
1302 self.auto_discover_schema(keyspace, table).await
1305 }
1306
1307 async fn auto_discover_schema(&self, keyspace: &str, table: &str) -> Result<TableSchema> {
1308 let sstable_files = self.find_sstable_files(keyspace, table).await?;
1311
1312 if sstable_files.is_empty() {
1313 return Err(Error::Schema(format!(
1314 "No SSTables found for {}.{}",
1315 keyspace, table
1316 )));
1317 }
1318
1319 self.discover_schema(keyspace, table, &sstable_files).await
1320 }
1321
1322 async fn find_sstable_files(&self, _keyspace: &str, _table: &str) -> Result<Vec<PathBuf>> {
1323 Ok(Vec::new())
1326 }
1327
1328 fn matches_query(&self, schema: &TableSchema, query: &SchemaQuery) -> bool {
1329 if let Some(ref ks) = query.keyspace {
1331 if &schema.keyspace != ks {
1332 return false;
1333 }
1334 }
1335
1336 if let Some(ref pattern) = query.table_pattern {
1338 if !self.matches_pattern(&schema.table, pattern) {
1339 return false;
1340 }
1341 }
1342
1343 true
1345 }
1346
1347 fn matches_pattern(&self, text: &str, pattern: &str) -> bool {
1348 if pattern == "*" {
1350 return true;
1351 }
1352
1353 text == pattern || text.contains(pattern)
1355 }
1356
1357 async fn create_schema_version(&self, table_id: &str, new_schema: &TableSchema) -> Result<()> {
1358 let mut version_history = self.version_history.write().await;
1359 let versions = version_history
1360 .entry(table_id.to_string())
1361 .or_insert_with(Vec::new);
1362
1363 let version_number = versions.len() as u32 + 1;
1364 let changes = if versions.is_empty() {
1365 vec![SchemaChange {
1366 change_type: SchemaChangeType::ColumnAdded,
1367 component: "initial".to_string(),
1368 description: "Initial schema version".to_string(),
1369 old_value: None,
1370 new_value: None,
1371 }]
1372 } else {
1373 self.detect_schema_changes(&versions.last().unwrap().schema, new_schema)
1375 };
1376
1377 let new_version = SchemaVersion {
1378 version: version_number,
1379 created_at: SystemTime::now(),
1380 schema: new_schema.clone(),
1381 changes,
1382 source: "registry".to_string(),
1383 };
1384
1385 versions.push(new_version);
1386
1387 if versions.len() > self.config.max_versions_per_schema {
1389 versions.remove(0);
1390 }
1391
1392 Ok(())
1393 }
1394
1395 fn detect_schema_changes(
1396 &self,
1397 old_schema: &TableSchema,
1398 new_schema: &TableSchema,
1399 ) -> Vec<SchemaChange> {
1400 let mut changes = Vec::new();
1401
1402 let old_columns: HashMap<_, _> = old_schema.columns.iter().map(|c| (&c.name, c)).collect();
1404 let new_columns: HashMap<_, _> = new_schema.columns.iter().map(|c| (&c.name, c)).collect();
1405
1406 for (name, column) in &new_columns {
1408 if !old_columns.contains_key(name) {
1409 changes.push(SchemaChange {
1410 change_type: SchemaChangeType::ColumnAdded,
1411 component: name.to_string(),
1412 description: format!(
1413 "Column '{}' added with type '{}'",
1414 name, column.data_type
1415 ),
1416 old_value: None,
1417 new_value: Some(column.data_type.clone()),
1418 });
1419 }
1420 }
1421
1422 for name in old_columns.keys() {
1424 if !new_columns.contains_key(name) {
1425 changes.push(SchemaChange {
1426 change_type: SchemaChangeType::ColumnRemoved,
1427 component: name.to_string(),
1428 description: format!("Column '{}' removed", name),
1429 old_value: None,
1430 new_value: None,
1431 });
1432 }
1433 }
1434
1435 for (name, new_column) in &new_columns {
1437 if let Some(old_column) = old_columns.get(name) {
1438 if old_column.data_type != new_column.data_type {
1439 changes.push(SchemaChange {
1440 change_type: SchemaChangeType::ColumnTypeChanged,
1441 component: name.to_string(),
1442 description: format!("Column '{}' type changed", name),
1443 old_value: Some(old_column.data_type.clone()),
1444 new_value: Some(new_column.data_type.clone()),
1445 });
1446 }
1447 }
1448 }
1449
1450 changes
1451 }
1452
1453 async fn validate_schema_udts(
1454 &self,
1455 schema: &TableSchema,
1456 errors: &mut Vec<ValidationError>,
1457 warnings: &mut Vec<ValidationWarning>,
1458 ) {
1459 let udt_registry = self.udt_registry.read().await;
1460
1461 for column in &schema.columns {
1462 if let Ok(cql_type) = CqlType::parse(&column.data_type) {
1464 self.validate_cql_type_udts(
1465 &cql_type,
1466 &schema.keyspace,
1467 &udt_registry,
1468 errors,
1469 warnings,
1470 );
1471 }
1472 }
1473 }
1474
1475 #[allow(clippy::only_used_in_recursion)]
1476 fn validate_cql_type_udts(
1477 &self,
1478 cql_type: &CqlType,
1479 keyspace: &str,
1480 udt_registry: &UdtRegistry,
1481 errors: &mut Vec<ValidationError>,
1482 _warnings: &mut Vec<ValidationWarning>,
1483 ) {
1484 match cql_type {
1485 CqlType::Udt(udt_name, _) => {
1486 if !udt_registry.contains_udt(keyspace, udt_name)
1487 && !udt_registry.contains_udt("system", udt_name)
1488 {
1489 errors.push(ValidationError {
1490 code: "UDT_NOT_FOUND".to_string(),
1491 message: format!("UDT '{}' not found in keyspace '{}'", udt_name, keyspace),
1492 component: Some(udt_name.clone()),
1493 severity: ErrorSeverity::High,
1494 });
1495 }
1496 }
1497 CqlType::Custom(name) => {
1504 let udt_name = name.strip_prefix("udt:").unwrap_or(name);
1505 let (lookup_keyspace, bare_name) = match udt_name.split_once('.') {
1506 Some((ks, n)) => (ks, n),
1507 None => (keyspace, udt_name),
1508 };
1509 if crate::schema::is_udt_identifier(udt_name)
1512 && !udt_registry.contains_udt(lookup_keyspace, bare_name)
1513 && !udt_registry.contains_udt("system", bare_name)
1514 {
1515 errors.push(ValidationError {
1516 code: "UDT_NOT_FOUND".to_string(),
1517 message: format!(
1518 "UDT '{}' not found in keyspace '{}'",
1519 udt_name, lookup_keyspace
1520 ),
1521 component: Some(udt_name.to_string()),
1522 severity: ErrorSeverity::High,
1523 });
1524 }
1525 }
1526 CqlType::List(inner) | CqlType::Set(inner) | CqlType::Frozen(inner) => {
1527 self.validate_cql_type_udts(inner, keyspace, udt_registry, errors, _warnings);
1528 }
1529 CqlType::Map(key_type, value_type) => {
1530 self.validate_cql_type_udts(key_type, keyspace, udt_registry, errors, _warnings);
1531 self.validate_cql_type_udts(value_type, keyspace, udt_registry, errors, _warnings);
1532 }
1533 CqlType::Tuple(types) => {
1534 for t in types {
1535 self.validate_cql_type_udts(t, keyspace, udt_registry, errors, _warnings);
1536 }
1537 }
1538 _ => {} }
1540 }
1541
1542 async fn validate_column_types(
1543 &self,
1544 schema: &TableSchema,
1545 errors: &mut Vec<ValidationError>,
1546 _warnings: &mut [ValidationWarning],
1547 ) {
1548 for column in &schema.columns {
1549 if let Err(e) = CqlType::parse(&column.data_type) {
1550 errors.push(ValidationError {
1551 code: "INVALID_COLUMN_TYPE".to_string(),
1552 message: format!("Invalid column type '{}': {}", column.data_type, e),
1553 component: Some(column.name.clone()),
1554 severity: ErrorSeverity::High,
1555 });
1556 }
1557 }
1558 }
1559
1560 async fn generate_performance_recommendations(
1561 &self,
1562 schema: &TableSchema,
1563 recommendations: &mut Vec<String>,
1564 ) {
1565 if schema.partition_keys.len() > 3 {
1569 recommendations.push(
1570 "Consider reducing the number of partition key columns for better performance"
1571 .to_string(),
1572 );
1573 }
1574
1575 if schema.clustering_keys.len() > 5 {
1577 recommendations
1578 .push("Large number of clustering keys may impact query performance".to_string());
1579 }
1580
1581 if schema.columns.len() > 50 {
1583 recommendations.push(
1584 "Consider using UDTs or denormalizing wide tables for better performance"
1585 .to_string(),
1586 );
1587 }
1588 }
1589
1590 fn generate_basic_cql(&self, schema: &TableSchema) -> String {
1591 let mut cql = format!("CREATE TABLE {}.{} (\n", schema.keyspace, schema.table);
1592
1593 for (i, column) in schema.columns.iter().enumerate() {
1595 if i > 0 {
1596 cql.push_str(",\n");
1597 }
1598 cql.push_str(&format!(" {} {}", column.name, column.data_type));
1599 }
1600
1601 if !schema.partition_keys.is_empty() {
1603 cql.push_str(",\n PRIMARY KEY (");
1604
1605 if schema.partition_keys.len() == 1 && schema.clustering_keys.is_empty() {
1606 cql.push_str(&schema.partition_keys[0].name);
1607 } else {
1608 cql.push('(');
1610 for (i, pk) in schema.partition_keys.iter().enumerate() {
1611 if i > 0 {
1612 cql.push_str(", ");
1613 }
1614 cql.push_str(&pk.name);
1615 }
1616 cql.push(')');
1617
1618 if !schema.clustering_keys.is_empty() {
1619 for ck in &schema.clustering_keys {
1620 cql.push_str(", ");
1621 cql.push_str(&ck.name);
1622 }
1623 }
1624 }
1625
1626 cql.push(')');
1627 }
1628
1629 cql.push_str("\n);");
1630 cql
1631 }
1632}
1633
1634#[derive(Debug, Clone)]
1636pub struct RegistryStatistics {
1637 pub total_schemas: usize,
1639 pub schemas_by_keyspace: HashMap<String, usize>,
1641 pub validated_schemas: usize,
1643 pub schemas_with_warnings: usize,
1645 pub invalid_schemas: usize,
1647 pub total_udts: usize,
1649 pub total_versions: usize,
1651 pub auto_discovered_schemas: usize,
1653 pub manually_registered_schemas: usize,
1655 pub cache_hit_rate: f64,
1657}
1658
1659#[derive(Debug, Clone)]
1665pub struct ParsingContext {
1666 pub schema: Arc<TableSchema>,
1668 pub partition_comparators: Arc<Vec<ComparatorType>>,
1670 pub clustering_comparators: Arc<Vec<ComparatorType>>,
1672 pub column_comparators: Arc<HashMap<String, ComparatorType>>,
1674}
1675
1676impl ParsingContext {
1677 pub fn from_owned(
1684 schema: TableSchema,
1685 partition_comparators: Vec<ComparatorType>,
1686 clustering_comparators: Vec<ComparatorType>,
1687 column_comparators: HashMap<String, ComparatorType>,
1688 ) -> Self {
1689 Self {
1690 schema: Arc::new(schema),
1691 partition_comparators: Arc::new(partition_comparators),
1692 clustering_comparators: Arc::new(clustering_comparators),
1693 column_comparators: Arc::new(column_comparators),
1694 }
1695 }
1696
1697 pub fn get_column_comparator(&self, column_name: &str) -> Option<&ComparatorType> {
1699 self.column_comparators.get(column_name)
1700 }
1701
1702 pub fn is_complete(&self) -> bool {
1704 !self.partition_comparators.is_empty() || !self.schema.partition_keys.is_empty()
1705 }
1706
1707 pub fn get_all_key_column_names(&self) -> Vec<String> {
1709 let mut names = Vec::new();
1710 names.extend(
1711 self.schema
1712 .ordered_partition_keys()
1713 .iter()
1714 .map(|k| k.name.clone()),
1715 );
1716 names.extend(
1717 self.schema
1718 .ordered_clustering_keys()
1719 .iter()
1720 .map(|k| k.name.clone()),
1721 );
1722 names
1723 }
1724}
1725
1726#[derive(Debug)]
1728pub struct SchemaValidator;
1729
1730impl Default for SchemaValidator {
1731 fn default() -> Self {
1732 Self::new()
1733 }
1734}
1735
1736impl SchemaValidator {
1737 pub fn new() -> Self {
1738 Self
1739 }
1740
1741 pub async fn validate_table_schema(&self, schema: &TableSchema) -> Result<()> {
1742 schema.validate()
1743 }
1744}
1745
1746#[cfg(test)]
1747mod tests {
1748 use super::*;
1749 use std::collections::HashMap;
1750
1751 async fn make_registry(mut reg_config: SchemaRegistryConfig) -> SchemaRegistry {
1752 reg_config.enable_auto_discovery = false;
1753 let core_config = Config::default();
1754 let platform = Arc::new(Platform::new(&core_config).await.expect("platform"));
1755 SchemaRegistry::new(reg_config, platform, core_config)
1756 .await
1757 .expect("registry")
1758 }
1759
1760 fn simple_schema(name: &str) -> TableSchema {
1761 TableSchema {
1762 keyspace: "test_ks".to_string(),
1763 table: name.to_string(),
1764 partition_keys: vec![crate::schema::KeyColumn {
1765 name: "id".to_string(),
1766 data_type: "uuid".to_string(),
1767 position: 0,
1768 }],
1769 clustering_keys: vec![],
1770 columns: vec![crate::schema::Column {
1771 name: "id".to_string(),
1772 data_type: "uuid".to_string(),
1773 nullable: false,
1774 default: None,
1775 is_static: false,
1776 }],
1777 comments: HashMap::new(),
1778 dropped_columns: HashMap::new(),
1779 }
1780 }
1781
1782 #[tokio::test]
1783 async fn test_schema_registry_creation() {
1784 let registry = make_registry(SchemaRegistryConfig::default()).await;
1785 let stats = registry.get_statistics().await.unwrap();
1786 assert_eq!(stats.total_schemas, 0);
1787 }
1788
1789 #[tokio::test]
1793 async fn test_registry_validate_lowercase_udt_not_found() {
1794 let registry = make_registry(SchemaRegistryConfig::default()).await;
1795 let udt_registry = UdtRegistry::new();
1796 let mut errors = Vec::new();
1797 let mut warnings = Vec::new();
1798
1799 let cql_type = CqlType::parse("address").expect("parse");
1800 registry.validate_cql_type_udts(
1801 &cql_type,
1802 "test_ks",
1803 &udt_registry,
1804 &mut errors,
1805 &mut warnings,
1806 );
1807
1808 assert!(
1809 errors
1810 .iter()
1811 .any(|e| e.code == "UDT_NOT_FOUND" && e.message.contains("address")),
1812 "undefined lowercase UDT must be reported, got: {errors:?}"
1813 );
1814 }
1815
1816 #[tokio::test]
1820 async fn test_registry_validate_uppercase_collection_udt_not_found() {
1821 let registry = make_registry(SchemaRegistryConfig::default()).await;
1822 let udt_registry = UdtRegistry::new();
1823
1824 for ty in ["LIST<MissingType>", "MAP<TEXT, MissingType>"] {
1825 let mut errors = Vec::new();
1826 let mut warnings = Vec::new();
1827 let cql_type = CqlType::parse(ty).expect("parse");
1828 registry.validate_cql_type_udts(
1829 &cql_type,
1830 "test_ks",
1831 &udt_registry,
1832 &mut errors,
1833 &mut warnings,
1834 );
1835 assert!(
1836 errors
1837 .iter()
1838 .any(|e| e.code == "UDT_NOT_FOUND" && e.message.contains("MissingType")),
1839 "undefined UDT in '{ty}' must be reported, got: {errors:?}"
1840 );
1841 }
1842 }
1843
1844 #[test]
1845 fn test_schema_query_creation() {
1846 let query = SchemaQuery {
1847 keyspace: Some("test_ks".to_string()),
1848 table_pattern: Some("user_*".to_string()),
1849 source_types: None,
1850 validated_only: false,
1851 include_history: false,
1852 };
1853
1854 assert_eq!(query.keyspace.as_ref().unwrap(), "test_ks");
1855 assert_eq!(query.table_pattern.as_ref().unwrap(), "user_*");
1856 }
1857
1858 #[tokio::test]
1859 async fn register_and_retrieve_schema() {
1860 let registry = make_registry(SchemaRegistryConfig::default()).await;
1861 let schema = simple_schema("users");
1862
1863 registry
1864 .register_schema(schema.clone(), SchemaSource::Manual)
1865 .await
1866 .expect("register schema");
1867
1868 let fetched = registry
1869 .get_schema("test_ks", "users")
1870 .await
1871 .expect("fetch schema");
1872 assert_eq!(fetched.table, "users");
1873 assert_eq!(fetched.partition_keys.len(), 1);
1874 }
1875
1876 #[tokio::test]
1885 async fn test_concurrent_register_schema_idempotent_no_deadlock() {
1886 let mut cfg = SchemaRegistryConfig::default();
1887 cfg.enable_versioning = true; let registry = Arc::new(make_registry(cfg).await);
1889
1890 registry
1892 .register_schema(simple_schema("users"), SchemaSource::Manual)
1893 .await
1894 .expect("seed register");
1895
1896 let mut handles = vec![];
1897 for _ in 0..16 {
1898 let r = Arc::clone(®istry);
1899 handles.push(tokio::spawn(async move {
1900 r.register_schema(simple_schema("users"), SchemaSource::Manual)
1901 .await
1902 .expect("concurrent register");
1903 }));
1904 }
1905 for h in handles {
1906 h.await.expect("task joins (no deadlock)");
1907 }
1908
1909 let all = registry.list_schemas(None).await.expect("list");
1911 assert_eq!(
1912 all.iter().filter(|s| s.table == "users").count(),
1913 1,
1914 "concurrent re-registration must leave exactly one entry"
1915 );
1916 }
1917
1918 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1930 async fn test_concurrent_first_registration_no_lost_version() {
1931 const N: usize = 8;
1932 for _ in 0..40 {
1933 let mut cfg = SchemaRegistryConfig::default();
1934 cfg.enable_versioning = true;
1935 cfg.max_versions_per_schema = 10_000;
1938 let registry = Arc::new(make_registry(cfg).await);
1939 let barrier = Arc::new(tokio::sync::Barrier::new(N));
1940
1941 let mut handles = vec![];
1942 for _ in 0..N {
1943 let r = Arc::clone(®istry);
1944 let b = Arc::clone(&barrier);
1945 handles.push(tokio::spawn(async move {
1946 b.wait().await;
1949 r.register_schema(simple_schema("users"), SchemaSource::Manual)
1950 .await
1951 .expect("concurrent register");
1952 }));
1953 }
1954 for h in handles {
1955 h.await.expect("task joins (no deadlock)");
1956 }
1957
1958 let all = registry.list_schemas(None).await.expect("list");
1960 assert_eq!(
1961 all.iter().filter(|s| s.table == "users").count(),
1962 1,
1963 "concurrent first registration must leave exactly one entry"
1964 );
1965
1966 let history = registry
1969 .get_schema_history("test_ks", "users")
1970 .await
1971 .expect("history");
1972 assert_eq!(
1973 history.len(),
1974 N - 1,
1975 "exactly one first-registration + (N-1) versioned updates: no lost update"
1976 );
1977 }
1978 }
1979
1980 fn schema_with_marker(table: &str, marker: &str) -> TableSchema {
1983 let mut s = simple_schema(table);
1984 s.columns.push(crate::schema::Column {
1985 name: marker.to_string(),
1986 data_type: "text".to_string(),
1987 nullable: true,
1988 default: None,
1989 is_static: false,
1990 });
1991 s
1992 }
1993
1994 fn column_names(schema: &TableSchema) -> Vec<String> {
1995 schema.columns.iter().map(|c| c.name.clone()).collect()
1996 }
1997
1998 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2009 async fn test_concurrent_distinct_schemas_registry_history_consistent() {
2010 const N: usize = 16;
2011 for _ in 0..2000 {
2012 let mut cfg = SchemaRegistryConfig::default();
2013 cfg.enable_versioning = true;
2014 cfg.max_versions_per_schema = 10_000; let registry = Arc::new(make_registry(cfg).await);
2016
2017 registry
2019 .register_schema(schema_with_marker("users", "seed"), SchemaSource::Manual)
2020 .await
2021 .expect("seed register");
2022
2023 let barrier = Arc::new(tokio::sync::Barrier::new(N));
2024 let mut handles = vec![];
2025 for i in 0..N {
2026 let r = Arc::clone(®istry);
2027 let b = Arc::clone(&barrier);
2028 handles.push(tokio::spawn(async move {
2029 let marker = format!("c{i}");
2030 b.wait().await;
2031 r.register_schema(schema_with_marker("users", &marker), SchemaSource::Manual)
2032 .await
2033 .expect("concurrent register");
2034 }));
2035 }
2036 for h in handles {
2037 h.await.expect("task joins (no deadlock)");
2038 }
2039
2040 let current = registry.get_schema("test_ks", "users").await.expect("get");
2044 let history = registry
2045 .get_schema_history("test_ks", "users")
2046 .await
2047 .expect("history");
2048 let newest = history.last().expect("at least one version recorded");
2049 assert_eq!(
2050 column_names(¤t),
2051 column_names(&newest.schema),
2052 "registry schema and newest version_history entry must agree \
2053 (registry={:?}, newest_version={:?})",
2054 column_names(¤t),
2055 column_names(&newest.schema),
2056 );
2057 }
2058 }
2059
2060 #[tokio::test]
2061 async fn schema_version_history_tracks_changes() {
2062 let registry = make_registry(SchemaRegistryConfig::default()).await;
2063 let mut schema = simple_schema("accounts");
2064
2065 registry
2066 .register_schema(schema.clone(), SchemaSource::Manual)
2067 .await
2068 .expect("register v1");
2069
2070 schema.columns.push(crate::schema::Column {
2071 name: "status".to_string(),
2072 data_type: "text".to_string(),
2073 nullable: true,
2074 default: None,
2075 is_static: false,
2076 });
2077
2078 registry
2079 .register_schema(schema.clone(), SchemaSource::Manual)
2080 .await
2081 .expect("register v2");
2082
2083 let history = registry
2084 .get_schema_history("test_ks", "accounts")
2085 .await
2086 .expect("history");
2087
2088 assert_eq!(
2089 history.len(),
2090 1,
2091 "Second registration should emit first version"
2092 );
2093 assert!(history[0]
2094 .changes
2095 .iter()
2096 .any(|change| matches!(change.change_type, SchemaChangeType::ColumnAdded)));
2097 }
2098
2099 fn multi_column_schema(name: &str) -> TableSchema {
2102 use crate::schema::{ClusteringColumn, ClusteringOrder, Column, KeyColumn};
2103 TableSchema {
2104 keyspace: "test_ks".to_string(),
2105 table: name.to_string(),
2106 partition_keys: vec![
2107 KeyColumn {
2108 name: "region".to_string(),
2109 data_type: "text".to_string(),
2110 position: 0,
2111 },
2112 KeyColumn {
2113 name: "shard".to_string(),
2114 data_type: "int".to_string(),
2115 position: 1,
2116 },
2117 ],
2118 clustering_keys: vec![
2119 ClusteringColumn {
2120 name: "ts".to_string(),
2121 data_type: "timestamp".to_string(),
2122 position: 0,
2123 order: ClusteringOrder::Asc,
2124 },
2125 ClusteringColumn {
2126 name: "seq".to_string(),
2127 data_type: "bigint".to_string(),
2128 position: 1,
2129 order: ClusteringOrder::Desc,
2130 },
2131 ],
2132 columns: vec![
2133 Column {
2134 name: "region".to_string(),
2135 data_type: "text".to_string(),
2136 nullable: false,
2137 default: None,
2138 is_static: false,
2139 },
2140 Column {
2141 name: "shard".to_string(),
2142 data_type: "int".to_string(),
2143 nullable: false,
2144 default: None,
2145 is_static: false,
2146 },
2147 Column {
2148 name: "ts".to_string(),
2149 data_type: "timestamp".to_string(),
2150 nullable: false,
2151 default: None,
2152 is_static: false,
2153 },
2154 Column {
2155 name: "seq".to_string(),
2156 data_type: "bigint".to_string(),
2157 nullable: false,
2158 default: None,
2159 is_static: false,
2160 },
2161 Column {
2162 name: "payload".to_string(),
2163 data_type: "text".to_string(),
2164 nullable: true,
2165 default: None,
2166 is_static: false,
2167 },
2168 Column {
2169 name: "tags".to_string(),
2170 data_type: "list<text>".to_string(),
2171 nullable: true,
2172 default: None,
2173 is_static: false,
2174 },
2175 ],
2176 comments: HashMap::new(),
2177 dropped_columns: HashMap::new(),
2178 }
2179 }
2180
2181 #[tokio::test]
2186 async fn get_parsing_context_does_zero_work_after_registration() {
2187 let registry = make_registry(SchemaRegistryConfig::default()).await;
2188 registry
2189 .register_schema(multi_column_schema("metrics"), SchemaSource::Manual)
2190 .await
2191 .expect("register");
2192
2193 crate::schema::work_counters::reset();
2195 let _ctx = registry
2196 .get_parsing_context("test_ks", "metrics")
2197 .await
2198 .expect("context");
2199
2200 assert_eq!(
2201 crate::schema::work_counters::parse_calls(),
2202 0,
2203 "get_parsing_context must not parse any CQL types after registration"
2204 );
2205 assert_eq!(
2206 crate::schema::work_counters::schema_clones(),
2207 0,
2208 "get_parsing_context must not deep-clone the schema after registration"
2209 );
2210
2211 crate::schema::work_counters::reset();
2213 let _ctx2 = registry
2214 .get_parsing_context("test_ks", "metrics")
2215 .await
2216 .expect("context2");
2217 assert_eq!(crate::schema::work_counters::parse_calls(), 0);
2218 assert_eq!(crate::schema::work_counters::schema_clones(), 0);
2219 }
2220
2221 #[tokio::test]
2224 async fn cached_parsing_context_matches_on_demand_derivation() {
2225 for schema in [simple_schema("simple"), multi_column_schema("wide")] {
2226 let table = schema.table.clone();
2227 let registry = make_registry(SchemaRegistryConfig::default()).await;
2228 registry
2229 .register_schema(schema, SchemaSource::Manual)
2230 .await
2231 .expect("register");
2232
2233 let ctx = registry
2234 .get_parsing_context("test_ks", &table)
2235 .await
2236 .expect("context");
2237
2238 let ref_schema = registry
2240 .get_schema("test_ks", &table)
2241 .await
2242 .expect("schema");
2243 let ref_partition = registry
2244 .get_partition_key_comparator("test_ks", &table)
2245 .await
2246 .expect("partition");
2247 let ref_clustering = registry
2248 .get_clustering_key_comparator("test_ks", &table)
2249 .await
2250 .expect("clustering");
2251 let ref_columns = registry
2252 .get_table_comparators("test_ks", &table)
2253 .await
2254 .expect("columns");
2255
2256 assert_eq!(
2259 serde_json::to_value(&*ctx.schema).expect("ctx schema json"),
2260 serde_json::to_value(&ref_schema).expect("ref schema json"),
2261 "schema mismatch for {}",
2262 table
2263 );
2264 assert_eq!(
2265 *ctx.partition_comparators, ref_partition,
2266 "partition comparators mismatch for {}",
2267 table
2268 );
2269 assert_eq!(
2270 *ctx.clustering_comparators, ref_clustering,
2271 "clustering comparators mismatch for {}",
2272 table
2273 );
2274 assert_eq!(
2275 *ctx.column_comparators, ref_columns,
2276 "column comparators mismatch for {}",
2277 table
2278 );
2279 }
2280 }
2281
2282 #[tokio::test]
2285 async fn parsing_contexts_share_arc_backing() {
2286 let registry = make_registry(SchemaRegistryConfig::default()).await;
2287 registry
2288 .register_schema(multi_column_schema("shared"), SchemaSource::Manual)
2289 .await
2290 .expect("register");
2291
2292 let a = registry
2293 .get_parsing_context("test_ks", "shared")
2294 .await
2295 .expect("a");
2296 let b = registry
2297 .get_parsing_context("test_ks", "shared")
2298 .await
2299 .expect("b");
2300
2301 assert!(Arc::ptr_eq(&a.schema, &b.schema));
2302 assert!(Arc::ptr_eq(
2303 &a.partition_comparators,
2304 &b.partition_comparators
2305 ));
2306 assert!(Arc::ptr_eq(
2307 &a.clustering_comparators,
2308 &b.clustering_comparators
2309 ));
2310 assert!(Arc::ptr_eq(&a.column_comparators, &b.column_comparators));
2311 }
2312
2313 #[tokio::test]
2314 async fn expired_cached_schema_invokes_discovery_path() {
2315 let mut config = SchemaRegistryConfig::default();
2316 config.cache_ttl_seconds = 0;
2317 config.enable_auto_discovery = true;
2318 let registry = make_registry(config).await;
2319
2320 registry
2321 .register_schema(simple_schema("events"), SchemaSource::Manual)
2322 .await
2323 .expect("register events schema");
2324
2325 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2326
2327 let err = registry
2328 .get_schema("test_ks", "events")
2329 .await
2330 .expect_err("expired schema should attempt discovery");
2331
2332 assert!(matches!(err, Error::Schema(message) if message.contains("No SSTables found")));
2333 }
2334
2335 #[tokio::test]
2344 async fn find_schema_by_table_honors_ttl_expiry_like_get_schema() {
2345 let mut config = SchemaRegistryConfig::default();
2346 config.cache_ttl_seconds = 0;
2347 let registry = make_registry(config).await;
2348
2349 registry
2350 .register_schema(simple_schema("events"), SchemaSource::Manual)
2351 .await
2352 .expect("register events schema");
2353
2354 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2355
2356 let get_err = registry
2358 .get_schema("test_ks", "events")
2359 .await
2360 .expect_err("expired schema attempts refresh");
2361 assert!(matches!(&get_err, Error::Schema(m) if m.contains("No SSTables found")));
2362
2363 let scoped = registry
2366 .find_schema_by_table(&Some("test_ks".to_string()), "events")
2367 .await;
2368 assert!(
2369 matches!(&scoped, Err(Error::Schema(m)) if m.contains("No SSTables found")),
2370 "scoped lookup on an EXPIRED entry must delegate to refresh (not serve stale), got {scoped:?}"
2371 );
2372
2373 let bare = registry.find_schema_by_table(&None, "events").await;
2375 assert!(
2376 matches!(&bare, Err(Error::Schema(m)) if m.contains("No SSTables found")),
2377 "bare lookup on an EXPIRED entry must delegate to refresh, got {bare:?}"
2378 );
2379 }
2380
2381 #[tokio::test]
2385 async fn find_schema_by_table_returns_fresh_and_none_for_unknown() {
2386 let registry = make_registry(SchemaRegistryConfig::default()).await;
2387 registry
2388 .register_schema(simple_schema("events"), SchemaSource::Manual)
2389 .await
2390 .expect("register");
2391
2392 let found = registry
2393 .find_schema_by_table(&Some("test_ks".to_string()), "events")
2394 .await
2395 .expect("a fresh entry never triggers the refresh error path");
2396 assert!(
2397 matches!(found, Some(s) if s.table == "events"),
2398 "fresh matched entry must be returned by value"
2399 );
2400
2401 let missing = registry
2402 .find_schema_by_table(&None, "never_registered")
2403 .await
2404 .expect("unknown table is Ok(None), not an error");
2405 assert!(
2406 missing.is_none(),
2407 "unknown table must be None (no fabrication, no auto-discovery)"
2408 );
2409 }
2410}