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
15mod cql_type_parser;
20mod key_ordering;
21mod schema_comparator;
22mod udt_registry;
23
24pub use udt_registry::{split_qualified_udt, udt_registry_from_cql, UdtRegistry};
25
26#[cfg(test)]
39pub(crate) mod work_counters {
40 use std::cell::Cell;
41
42 thread_local! {
43 static PARSE_CALLS: Cell<usize> = const { Cell::new(0) };
45 static SCHEMA_CLONES: Cell<usize> = const { Cell::new(0) };
48 }
49
50 pub(crate) fn record_parse_call() {
51 PARSE_CALLS.with(|c| c.set(c.get() + 1));
52 }
53
54 pub(crate) fn record_schema_clone() {
55 SCHEMA_CLONES.with(|c| c.set(c.get() + 1));
56 }
57
58 pub(crate) fn reset() {
60 PARSE_CALLS.with(|c| c.set(0));
61 SCHEMA_CLONES.with(|c| c.set(0));
62 }
63
64 pub(crate) fn parse_calls() -> usize {
65 PARSE_CALLS.with(|c| c.get())
66 }
67
68 pub(crate) fn schema_clones() -> usize {
69 SCHEMA_CLONES.with(|c| c.get())
70 }
71}
72
73pub use aggregator::{
75 AggregatorConfig, LoadErrorType, LoadResult, SchemaAggregator, SchemaLoadError,
76 SchemaLoadWarning,
77};
78
79pub use cql_parser::{
81 cql_type_to_type_id, extract_table_name, parse_cql_schema, parse_cql_schema_with_visitor,
82 parse_create_table, table_name_matches,
83};
84
85pub use discovery::{
87 ColumnDefinition, DiscoveryMethod, IndexDefinition, SchemaDiscoveryConfig,
88 SchemaDiscoveryEngine, SchemaInfo, SchemaMetadata, TableOptions, TypeInfo, UDTDefinition,
89 ValidationError, ValidationResults, ValidationStatus, ValidationWarning,
90};
91
92pub use registry::{
93 ParsingContext, RegistryStatistics, SchemaChange, SchemaChangeType, SchemaQuery,
94 SchemaRegistry, SchemaRegistryConfig, SchemaSource, SchemaValidationStatus, SchemaValidator,
95 SchemaVersion, ValidationReport,
96};
97
98#[cfg(test)]
104thread_local! {
105 pub(crate) static TABLE_SCHEMA_CLONES: std::cell::Cell<usize> =
106 const { std::cell::Cell::new(0) };
107}
108
109pub use parser::SchemaParser;
110
111#[cfg(feature = "experimental")]
112pub use json_exporter::{
113 JsonClusteringKey, JsonColumn, JsonExportConfig, JsonExporter, JsonFormat, JsonIndex,
114 JsonMetadata, JsonPerformanceMetrics, JsonPrimaryKey, JsonSchema, JsonTable, JsonTableOptions,
115 JsonUDT, JsonValidationResults,
116};
117
118pub type ColumnSpec = Column;
120
121use crate::error::{Error, Result};
122use crate::parser::header::SSTableHeader;
123use crate::parser::types::CqlTypeId;
124use crate::storage::StorageEngine;
125use crate::types::UdtTypeDef;
126use crate::Config;
127use serde::{Deserialize, Serialize};
128use std::collections::HashMap;
129use std::fs;
130use std::path::Path;
131use std::sync::Arc;
132use tokio::sync::RwLock;
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct TableSchema {
137 pub keyspace: String,
139
140 pub table: String,
142
143 pub partition_keys: Vec<KeyColumn>,
145
146 pub clustering_keys: Vec<ClusteringColumn>,
148
149 pub columns: Vec<Column>,
151
152 #[serde(default)]
154 pub comments: HashMap<String, String>,
155
156 #[serde(default)]
180 pub dropped_columns: HashMap<String, i64>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct KeyColumn {
186 pub name: String,
188
189 #[serde(rename = "type")]
191 pub data_type: String,
192
193 pub position: usize,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ClusteringColumn {
200 pub name: String,
202
203 #[serde(rename = "type")]
205 pub data_type: String,
206
207 pub position: usize,
209
210 #[serde(default)]
212 pub order: ClusteringOrder,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
217pub enum ClusteringOrder {
218 #[default]
220 Asc,
221 Desc,
223}
224
225impl From<&str> for ClusteringOrder {
226 fn from(s: &str) -> Self {
227 match s.to_uppercase().as_str() {
228 "DESC" => ClusteringOrder::Desc,
229 _ => ClusteringOrder::Asc,
230 }
231 }
232}
233
234impl std::fmt::Display for ClusteringOrder {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 match self {
237 ClusteringOrder::Asc => write!(f, "ASC"),
238 ClusteringOrder::Desc => write!(f, "DESC"),
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct Column {
246 pub name: String,
248
249 #[serde(rename = "type")]
251 pub data_type: String,
252
253 #[serde(default)]
255 pub nullable: bool,
256
257 #[serde(default)]
259 pub default: Option<serde_json::Value>,
260
261 #[serde(default)]
263 pub is_static: bool,
264}
265
266#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
268pub enum CqlType {
269 Boolean,
271 TinyInt,
272 SmallInt,
273 Int,
274 BigInt,
275 Counter,
276 Float,
277 Double,
278 Decimal,
279 Text,
280 Ascii,
281 Varchar,
282 Blob,
283 Timestamp,
284 Date,
285 Time,
286 Uuid,
287 TimeUuid,
288 Inet,
289 Duration,
290 Varint,
291
292 List(Box<CqlType>),
294 Set(Box<CqlType>),
295 Map(Box<CqlType>, Box<CqlType>),
296
297 Tuple(Vec<CqlType>),
299 Udt(String, Vec<(String, CqlType)>), Frozen(Box<CqlType>),
301
302 Custom(String),
304}
305
306pub(crate) fn is_udt_identifier(name: &str) -> bool {
314 !name.is_empty()
315 && name
316 .chars()
317 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
318}
319
320impl TableSchema {
321 pub fn from_sstable_header(header: &SSTableHeader) -> Result<Self> {
326 let mut partition_keys = Vec::new();
328 let mut clustering_keys = Vec::new();
329 let mut regular_columns = Vec::new();
330
331 for col_info in &header.columns {
332 if col_info.is_primary_key {
333 if col_info.is_clustering {
334 clustering_keys.push(col_info);
335 } else {
336 partition_keys.push(col_info);
337 }
338 } else {
339 regular_columns.push(col_info);
340 }
341 }
342
343 for col_info in &partition_keys {
345 if col_info.key_position.is_none() {
346 return Err(Error::schema(format!(
347 "Partition key column '{}' missing key_position in SSTable header",
348 col_info.name
349 )));
350 }
351 }
352
353 for col_info in &clustering_keys {
355 if col_info.key_position.is_none() {
356 return Err(Error::schema(format!(
357 "Clustering key column '{}' missing key_position in SSTable header",
358 col_info.name
359 )));
360 }
361 }
362
363 partition_keys.sort_by_key(|c| c.key_position.unwrap());
365 clustering_keys.sort_by_key(|c| c.key_position.unwrap());
366
367 let partition_keys: Vec<KeyColumn> = partition_keys
370 .iter()
371 .enumerate()
372 .map(|(pos, col)| KeyColumn {
373 name: col.name.clone(),
374 data_type: col.column_type.clone(),
375 position: pos, })
377 .collect();
378
379 let clustering_keys: Vec<ClusteringColumn> = clustering_keys
381 .iter()
382 .enumerate()
383 .map(|(pos, col)| ClusteringColumn {
384 name: col.name.clone(),
385 data_type: col.column_type.clone(),
386 position: pos, order: if col.clustering_reversed {
393 ClusteringOrder::Desc
394 } else {
395 ClusteringOrder::Asc
396 },
397 })
398 .collect();
399
400 let columns: Vec<Column> = header
402 .columns
403 .iter()
404 .map(|col| Column {
405 name: col.name.clone(),
406 data_type: col.column_type.clone(),
407 nullable: !col.is_primary_key, default: None,
409 is_static: col.is_static,
413 })
414 .collect();
415
416 if partition_keys.is_empty() {
417 return Err(Error::schema(
418 "No partition keys found in SSTable header".to_string(),
419 ));
420 }
421
422 let schema = TableSchema {
423 keyspace: header.keyspace.clone(),
424 table: header.table_name.clone(),
425 partition_keys,
426 clustering_keys,
427 columns,
428 comments: HashMap::new(),
429 dropped_columns: HashMap::new(),
430 };
431
432 schema.validate()?;
433 Ok(schema)
434 }
435
436 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
438 let content = fs::read_to_string(path)
439 .map_err(|e| Error::schema(format!("Failed to read schema file: {}", e)))?;
440
441 Self::from_json(&content)
442 }
443
444 pub fn from_json(json: &str) -> Result<Self> {
446 let schema: TableSchema = serde_json::from_str(json)
447 .map_err(|e| Error::schema(format!("Invalid JSON schema: {}", e)))?;
448
449 schema.validate()?;
450 Ok(schema)
451 }
452
453 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
455 let json = serde_json::to_string_pretty(self)
456 .map_err(|e| Error::serialization(format!("Failed to serialize schema: {}", e)))?;
457
458 fs::write(path, json)
459 .map_err(|e| Error::schema(format!("Failed to write schema file: {}", e)))?;
460
461 Ok(())
462 }
463
464 pub fn validate(&self) -> Result<()> {
466 if self.keyspace.is_empty() {
468 return Err(Error::schema("Keyspace name cannot be empty".to_string()));
469 }
470
471 if self.table.is_empty() {
472 return Err(Error::schema("Table name cannot be empty".to_string()));
473 }
474
475 if self.partition_keys.is_empty() {
477 return Err(Error::schema(
478 "Table must have at least one partition key".to_string(),
479 ));
480 }
481
482 let mut positions: Vec<_> = self.partition_keys.iter().map(|k| k.position).collect();
484 positions.sort();
485 for (i, &pos) in positions.iter().enumerate() {
486 if pos != i {
487 return Err(Error::schema(format!(
488 "Partition key positions must be contiguous starting from 0, found gap at position {}",
489 i
490 )));
491 }
492 }
493
494 if !self.clustering_keys.is_empty() {
496 let mut positions: Vec<_> = self.clustering_keys.iter().map(|k| k.position).collect();
497 positions.sort();
498 for (i, &pos) in positions.iter().enumerate() {
499 if pos != i {
500 return Err(Error::schema(format!(
501 "Clustering key positions must be contiguous starting from 0, found gap at position {}",
502 i
503 )));
504 }
505 }
506 }
507
508 for column in &self.columns {
510 CqlType::parse(&column.data_type).map_err(|e| {
511 Error::schema(format!(
512 "Invalid data type '{}' for column '{}': {}",
513 column.data_type, column.name, e
514 ))
515 })?;
516 }
517
518 for key in &self.partition_keys {
524 if !self.columns.iter().any(|c| c.name == key.name) {
525 return Err(Error::schema(format!(
526 "Partition key '{}' not found in columns list",
527 key.name
528 )));
529 }
530 }
531
532 for key in &self.clustering_keys {
533 if !self.columns.iter().any(|c| c.name == key.name) {
534 return Err(Error::schema(format!(
535 "Clustering key '{}' not found in columns list",
536 key.name
537 )));
538 }
539 }
540
541 self.validate_dropped_columns()?;
542
543 Ok(())
544 }
545
546 pub fn for_compaction_output(
570 &self,
571 retained: &std::collections::HashSet<String>,
572 ) -> TableSchema {
573 TableSchema {
574 keyspace: self.keyspace.clone(),
575 table: self.table.clone(),
576 partition_keys: self.partition_keys.clone(),
577 clustering_keys: self.clustering_keys.clone(),
578 columns: self
579 .columns
580 .iter()
581 .filter(|c| {
582 !self.dropped_columns.contains_key(&c.name) || retained.contains(&c.name)
585 })
586 .cloned()
587 .collect(),
588 comments: self.comments.clone(),
589 dropped_columns: HashMap::new(),
590 }
591 }
592
593 pub fn validate_dropped_columns(&self) -> Result<()> {
609 for name in self.dropped_columns.keys() {
610 if !self.columns.iter().any(|c| &c.name == name) {
611 return Err(Error::schema(format!(
612 "dropped column '{}' must remain declared in `columns` (with its type) so \
613 its cells can be decoded and purged during compaction; a dropped column \
614 present only in `dropped_columns` cannot be decoded (see #904/#847)",
615 name
616 )));
617 }
618 }
619 Ok(())
620 }
621
622 pub fn validate_udt_references(&self, registry: &UdtRegistry) -> Result<()> {
634 for column in &self.columns {
635 if let Ok(cql_type) = CqlType::parse(&column.data_type) {
638 self.check_type_udt_references(&cql_type, &column.name, registry)?;
639 }
640 }
641 Ok(())
642 }
643
644 fn check_type_udt_references(
647 &self,
648 cql_type: &CqlType,
649 column_name: &str,
650 registry: &UdtRegistry,
651 ) -> Result<()> {
652 match cql_type {
653 CqlType::Udt(name, _) => {
654 self.ensure_udt_exists(name, column_name, registry)?;
655 }
656 CqlType::Custom(name) => {
665 let udt_name = name.strip_prefix("udt:").unwrap_or(name);
666 if is_udt_identifier(udt_name) {
667 self.ensure_udt_exists(udt_name, column_name, registry)?;
668 }
669 }
670 CqlType::List(inner) | CqlType::Set(inner) | CqlType::Frozen(inner) => {
671 self.check_type_udt_references(inner, column_name, registry)?;
672 }
673 CqlType::Map(key_type, value_type) => {
674 self.check_type_udt_references(key_type, column_name, registry)?;
675 self.check_type_udt_references(value_type, column_name, registry)?;
676 }
677 CqlType::Tuple(field_types) => {
678 for field_type in field_types {
679 self.check_type_udt_references(field_type, column_name, registry)?;
680 }
681 }
682 _ => {} }
684 Ok(())
685 }
686
687 fn ensure_udt_exists(
690 &self,
691 udt_name: &str,
692 column_name: &str,
693 registry: &UdtRegistry,
694 ) -> Result<()> {
695 let (lookup_keyspace, bare_name) =
699 crate::schema::split_qualified_udt(udt_name, self.keyspace.as_str());
700
701 if registry.contains_udt(lookup_keyspace, bare_name)
702 || registry.contains_udt("system", bare_name)
703 {
704 return Ok(());
705 }
706
707 Err(Error::schema(format!(
708 "Column '{}' references undefined UDT '{}' in keyspace '{}'",
709 column_name, udt_name, lookup_keyspace
710 )))
711 }
712
713 pub fn get_column(&self, name: &str) -> Option<&Column> {
715 self.columns.iter().find(|c| c.name == name)
716 }
717
718 pub fn is_partition_key(&self, name: &str) -> bool {
720 self.partition_keys.iter().any(|k| k.name == name)
721 }
722
723 pub fn is_clustering_key(&self, name: &str) -> bool {
725 self.clustering_keys.iter().any(|k| k.name == name)
726 }
727
728 #[cfg(test)]
734 pub fn new_for_testing(keyspace: &str, table: &str) -> Self {
735 Self {
736 keyspace: keyspace.to_string(),
737 table: table.to_string(),
738 partition_keys: vec![KeyColumn {
739 name: "id".to_string(),
740 data_type: "int".to_string(),
741 position: 0,
742 }],
743 clustering_keys: vec![],
744 columns: vec![Column {
745 name: "id".to_string(),
746 data_type: "int".to_string(),
747 nullable: false,
748 default: None,
749 is_static: false,
750 }],
751 comments: HashMap::new(),
752 dropped_columns: HashMap::new(),
753 }
754 }
755}
756
757#[derive(Debug)]
767pub struct SchemaManager {
768 #[allow(dead_code)]
769 storage: Arc<StorageEngine>,
770 registry: Arc<RwLock<registry::SchemaRegistry>>,
772}
773
774impl SchemaManager {
775 async fn default_registry(
777 platform: Arc<crate::platform::Platform>,
778 config: &Config,
779 ) -> Result<Arc<RwLock<registry::SchemaRegistry>>> {
780 let registry = registry::SchemaRegistry::new(
781 registry::SchemaRegistryConfig::default(),
782 platform,
783 config.clone(),
784 )
785 .await?;
786 Ok(Arc::new(RwLock::new(registry)))
787 }
788
789 pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
791 let config = Config::default();
793 let platform = Arc::new(crate::platform::Platform::new(&config).await?);
794 let storage = Arc::new(
795 StorageEngine::open(
796 path.as_ref(),
797 &config,
798 platform.clone(),
799 #[cfg(feature = "state_machine")]
800 None,
801 )
802 .await?,
803 );
804
805 let registry = Self::default_registry(platform, &config).await?;
806 Ok(Self { storage, registry })
807 }
808
809 pub async fn new_with_storage(storage: Arc<StorageEngine>, config: &Config) -> Result<Self> {
811 let platform = Arc::new(crate::platform::Platform::new(config).await?);
812 let registry = Self::default_registry(platform, config).await?;
813 let manager = Self { storage, registry };
814
815 manager.load_default_udts().await;
817
818 Ok(manager)
819 }
820
821 pub async fn new_with_registry(
836 storage: Arc<StorageEngine>,
837 registry: Arc<tokio::sync::RwLock<registry::SchemaRegistry>>,
838 _config: &Config,
839 ) -> Result<Self> {
840 Ok(Self { storage, registry })
841 }
842
843 #[cfg(test)]
848 pub(crate) fn registry(&self) -> Arc<RwLock<registry::SchemaRegistry>> {
849 self.registry.clone()
850 }
851
852 async fn load_default_udts(&self) {
854 let address_udt = UdtTypeDef::new("test_keyspace".to_string(), "address".to_string())
856 .with_field("street".to_string(), CqlType::Text, true)
857 .with_field("city".to_string(), CqlType::Text, true)
858 .with_field("state".to_string(), CqlType::Text, true)
859 .with_field("zip_code".to_string(), CqlType::Text, true)
860 .with_field("country".to_string(), CqlType::Text, true);
861
862 self.register_udt(address_udt).await;
863
864 let person_udt = UdtTypeDef::new("test_keyspace".to_string(), "person".to_string())
866 .with_field("name".to_string(), CqlType::Text, true)
867 .with_field("age".to_string(), CqlType::Int, true)
868 .with_field("email".to_string(), CqlType::Text, true)
869 .with_field(
870 "addresses".to_string(),
871 CqlType::List(Box::new(CqlType::Udt(
872 "address".to_string(),
873 vec![
874 ("street".to_string(), CqlType::Text),
875 ("city".to_string(), CqlType::Text),
876 ("state".to_string(), CqlType::Text),
877 ("zip_code".to_string(), CqlType::Text),
878 ("country".to_string(), CqlType::Text),
879 ],
880 ))),
881 true,
882 )
883 .with_field(
884 "contact_info".to_string(),
885 CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::Text)),
886 true,
887 );
888
889 self.register_udt(person_udt).await;
890
891 let company_udt = UdtTypeDef::new("test_keyspace".to_string(), "company".to_string())
893 .with_field("name".to_string(), CqlType::Text, false)
894 .with_field(
895 "headquarters".to_string(),
896 CqlType::Udt(
897 "address".to_string(),
898 vec![
899 ("street".to_string(), CqlType::Text),
900 ("city".to_string(), CqlType::Text),
901 ("state".to_string(), CqlType::Text),
902 ("zip_code".to_string(), CqlType::Text),
903 ("country".to_string(), CqlType::Text),
904 ],
905 ),
906 true,
907 )
908 .with_field(
909 "employees".to_string(),
910 CqlType::Set(Box::new(CqlType::Udt("person".to_string(), vec![]))),
911 true,
912 )
913 .with_field("founded_year".to_string(), CqlType::Int, true);
914
915 self.register_udt(company_udt).await;
916 }
917
918 pub async fn register_udt(&self, udt_def: UdtTypeDef) {
920 let _ = self.registry.read().await.register_udt(udt_def).await;
923 }
924
925 pub async fn get_udt(&self, keyspace: &str, name: &str) -> Option<UdtTypeDef> {
927 self.registry
928 .read()
929 .await
930 .get_udt(keyspace, name)
931 .await
932 .ok()
933 .flatten()
934 }
935
936 pub async fn load_schema(&self, table_name: &str) -> Result<TableSchema> {
948 let (keyspace, table) = match table_name.split_once('.') {
951 Some((ks, tbl)) => (Some(ks.to_string()), tbl),
952 None => (None, table_name),
953 };
954 self.find_schema_by_table(&keyspace, table)
955 .await?
956 .ok_or_else(|| {
957 Error::schema(format!(
958 "unknown table {}; no schema registered or discovered",
959 table_name
960 ))
961 })
962 }
963
964 pub async fn parse_and_register_cql_schema(&self, cql: &str) -> Result<TableSchema> {
970 let schema = cql_parser::parse_cql_schema(cql)?;
971 self.registry
972 .read()
973 .await
974 .register_schema(schema.clone(), registry::SchemaSource::Cql(cql.to_string()))
975 .await?;
976 Ok(schema)
977 }
978
979 pub async fn find_schema_by_table(
987 &self,
988 keyspace: &Option<String>,
989 table: &str,
990 ) -> Result<Option<TableSchema>> {
991 let found = self
992 .registry
993 .read()
994 .await
995 .find_schema_by_table(keyspace, table)
996 .await?;
997 #[cfg(test)]
1000 if found.is_some() {
1001 TABLE_SCHEMA_CLONES.with(|c| c.set(c.get() + 1));
1002 }
1003 Ok(found)
1004 }
1005
1006 pub fn extract_table_info(&self, cql: &str) -> Result<(Option<String>, String)> {
1008 cql_parser::extract_table_name(cql)
1009 }
1010
1011 pub fn cql_type_to_internal(&self, cql_type: &str) -> Result<CqlTypeId> {
1013 cql_parser::cql_type_to_type_id(cql_type)
1014 }
1015
1016 pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema> {
1018 if let Some(schema) = self.find_schema_by_table(&None, table_name).await? {
1020 Ok(schema)
1021 } else {
1022 Err(Error::Schema(format!(
1023 "Table schema not found: {}",
1024 table_name
1025 )))
1026 }
1027 }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use super::*;
1033
1034 #[test]
1035 fn test_schema_validation() {
1036 let schema_json = r#"
1037 {
1038 "keyspace": "test",
1039 "table": "users",
1040 "partition_keys": [
1041 {"name": "id", "type": "bigint", "position": 0}
1042 ],
1043 "clustering_keys": [],
1044 "columns": [
1045 {"name": "id", "type": "bigint", "nullable": false},
1046 {"name": "name", "type": "text", "nullable": true}
1047 ]
1048 }
1049 "#;
1050
1051 let schema = TableSchema::from_json(schema_json).unwrap();
1052 assert_eq!(schema.keyspace, "test");
1053 assert_eq!(schema.table, "users");
1054 assert_eq!(schema.partition_keys.len(), 1);
1055 assert_eq!(schema.columns.len(), 2);
1056 }
1057
1058 #[test]
1059 fn test_schema_validation_failures() {
1060 let invalid_schema = r#"
1062 {
1063 "keyspace": "test",
1064 "table": "users",
1065 "partition_keys": [],
1066 "clustering_keys": [],
1067 "columns": []
1068 }
1069 "#;
1070
1071 assert!(TableSchema::from_json(invalid_schema).is_err());
1072
1073 let invalid_type = r#"
1075 {
1076 "keyspace": "test",
1077 "table": "users",
1078 "partition_keys": [
1079 {"name": "id", "type": "invalid_type", "position": 0}
1080 ],
1081 "clustering_keys": [],
1082 "columns": [
1083 {"name": "id", "type": "invalid_type", "nullable": false}
1084 ]
1085 }
1086 "#;
1087
1088 assert!(TableSchema::from_json(invalid_type).is_ok());
1090 }
1091
1092 fn udt_schema(column_type: &str) -> TableSchema {
1093 TableSchema {
1094 keyspace: "test_ks".to_string(),
1095 table: "t".to_string(),
1096 partition_keys: vec![KeyColumn {
1097 name: "id".to_string(),
1098 data_type: "uuid".to_string(),
1099 position: 0,
1100 }],
1101 clustering_keys: vec![],
1102 columns: vec![
1103 Column {
1104 name: "id".to_string(),
1105 data_type: "uuid".to_string(),
1106 nullable: false,
1107 default: None,
1108 is_static: false,
1109 },
1110 Column {
1111 name: "value".to_string(),
1112 data_type: column_type.to_string(),
1113 nullable: true,
1114 default: None,
1115 is_static: false,
1116 },
1117 ],
1118 comments: HashMap::new(),
1119 dropped_columns: HashMap::new(),
1120 }
1121 }
1122
1123 #[test]
1124 fn test_udt_reference_undefined_top_level_errors() {
1125 let registry = UdtRegistry::new();
1126 let schema = udt_schema("MyMissingType");
1127
1128 let err = schema
1129 .validate_udt_references(®istry)
1130 .expect_err("undefined UDT reference must fail validation");
1131 let msg = err.to_string();
1132 assert!(
1133 matches!(err, Error::Schema(_)),
1134 "expected schema-category error, got {err:?}"
1135 );
1136 assert!(
1137 msg.contains("MyMissingType"),
1138 "error must name the missing UDT, got: {msg}"
1139 );
1140 }
1141
1142 #[test]
1143 fn test_udt_reference_undefined_lowercase_errors() {
1144 let registry = UdtRegistry::new();
1149 for col_type in ["address", "list<frozen<address>>"] {
1150 let schema = udt_schema(col_type);
1151 let err = schema
1152 .validate_udt_references(®istry)
1153 .expect_err("undefined lowercase UDT must fail validation");
1154 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1155 assert!(
1156 err.to_string().contains("address"),
1157 "error must name the missing UDT, got: {err}"
1158 );
1159 }
1160 }
1161
1162 #[test]
1163 fn test_uppercase_collection_of_primitives_is_not_a_udt() {
1164 let registry = UdtRegistry::new();
1167 for col_type in [
1168 "SET<TEXT>",
1169 "LIST<INT>",
1170 "MAP<TEXT, TEXT>",
1171 "FROZEN<LIST<INT>>",
1172 ] {
1173 let schema = udt_schema(col_type);
1174 schema
1175 .validate_udt_references(®istry)
1176 .unwrap_or_else(|e| panic!("'{col_type}' must not be flagged as a UDT: {e}"));
1177 }
1178 }
1179
1180 #[test]
1181 fn test_uppercase_collection_with_undefined_udt_errors() {
1182 let registry = UdtRegistry::new();
1186 for col_type in [
1187 "LIST<MissingType>",
1188 "MAP<TEXT, MissingType>",
1189 "FROZEN<SET<MissingType>>",
1190 ] {
1191 let schema = udt_schema(col_type);
1192 let err = schema
1193 .validate_udt_references(®istry)
1194 .expect_err("undefined UDT in uppercase collection must fail");
1195 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1196 assert!(
1197 err.to_string().contains("MissingType"),
1198 "error must name the missing UDT, got: {err}"
1199 );
1200 }
1201 }
1202
1203 #[test]
1204 fn test_udt_reference_undefined_nested_in_collection_errors() {
1205 let registry = UdtRegistry::new();
1206 let schema = udt_schema("list<frozen<NestedMissing>>");
1207
1208 let err = schema
1209 .validate_udt_references(®istry)
1210 .expect_err("nested undefined UDT reference must fail validation");
1211 let msg = err.to_string();
1212 assert!(matches!(err, Error::Schema(_)));
1213 assert!(
1214 msg.contains("NestedMissing"),
1215 "error must name the nested missing UDT, got: {msg}"
1216 );
1217 }
1218
1219 #[test]
1220 fn test_udt_reference_undefined_nested_in_map_errors() {
1221 let registry = UdtRegistry::new();
1222 let schema = udt_schema("map<text, MapValueMissing>");
1223
1224 let err = schema
1225 .validate_udt_references(®istry)
1226 .expect_err("undefined UDT in map value must fail validation");
1227 assert!(matches!(err, Error::Schema(_)));
1228 assert!(err.to_string().contains("MapValueMissing"));
1229 }
1230
1231 #[test]
1232 fn test_udt_reference_defined_top_level_ok() {
1233 let mut registry = UdtRegistry::new();
1234 registry.register_udt(
1235 UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1236 "a".to_string(),
1237 CqlType::Text,
1238 true,
1239 ),
1240 );
1241 let schema = udt_schema("MyType");
1242 schema
1243 .validate_udt_references(®istry)
1244 .expect("defined UDT should validate");
1245 }
1246
1247 #[test]
1248 fn test_udt_reference_defined_nested_ok() {
1249 let mut registry = UdtRegistry::new();
1250 registry.register_udt(
1251 UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1252 "a".to_string(),
1253 CqlType::Text,
1254 true,
1255 ),
1256 );
1257 let schema = udt_schema("list<frozen<MyType>>");
1258 schema
1259 .validate_udt_references(®istry)
1260 .expect("defined nested UDT should validate");
1261 }
1262
1263 #[test]
1264 fn test_validate_udt_references_no_udts_ok() {
1265 let registry = UdtRegistry::new();
1267 let schema = udt_schema("map<text, list<int>>");
1268 schema
1269 .validate_udt_references(®istry)
1270 .expect("primitive/collection-only schema should validate");
1271 }
1272
1273 async fn build_storage() -> Arc<StorageEngine> {
1274 let config = Config::default();
1275 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1276 let temp_dir = tempfile::tempdir().unwrap();
1277 Arc::new(
1278 StorageEngine::open(
1279 temp_dir.path(),
1280 &config,
1281 platform,
1282 #[cfg(feature = "state_machine")]
1283 None,
1284 )
1285 .await
1286 .unwrap(),
1287 )
1288 }
1289
1290 async fn build_shared_registry() -> Arc<RwLock<registry::SchemaRegistry>> {
1291 let config = Config::default();
1292 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1293 Arc::new(RwLock::new(
1294 registry::SchemaRegistry::new(
1295 registry::SchemaRegistryConfig::default(),
1296 platform,
1297 config,
1298 )
1299 .await
1300 .unwrap(),
1301 ))
1302 }
1303
1304 async fn test_manager() -> Arc<SchemaManager> {
1305 let config = Config::default();
1306 let storage = build_storage().await;
1307 Arc::new(
1308 SchemaManager::new_with_storage(storage, &config)
1309 .await
1310 .unwrap(),
1311 )
1312 }
1313
1314 fn table_schema_named(table_name: &str) -> TableSchema {
1315 let mut schema = udt_schema("text");
1316 schema.table = table_name.to_string();
1317 schema
1318 }
1319
1320 #[tokio::test]
1324 async fn test_load_schema_unknown_table_errs() {
1325 let manager = test_manager().await;
1326
1327 let err = manager
1328 .load_schema("never_defined_table")
1329 .await
1330 .expect_err("unknown table must error, not fabricate a schema");
1331 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1332 assert!(
1333 err.to_string().contains("never_defined_table"),
1334 "error must name the unknown table, got: {err}"
1335 );
1336
1337 let stats = manager
1339 .registry()
1340 .read()
1341 .await
1342 .get_statistics()
1343 .await
1344 .unwrap();
1345 assert_eq!(
1346 stats.total_schemas, 0,
1347 "unknown-table lookup must not mutate the registry"
1348 );
1349 }
1350
1351 #[tokio::test]
1354 async fn test_concurrent_schema_access() {
1355 let manager = test_manager().await;
1356
1357 for i in 0..3 {
1360 let name = format!("table_{}", i);
1361 manager
1362 .registry()
1363 .read()
1364 .await
1365 .register_schema(table_schema_named(&name), registry::SchemaSource::Manual)
1366 .await
1367 .unwrap();
1368 }
1369
1370 let mut handles = vec![];
1372 for i in 0..10 {
1373 let m = Arc::clone(&manager);
1374 let handle = tokio::spawn(async move {
1375 let table = format!("table_{}", i % 3);
1376 let schema = m.load_schema(&table).await.expect("registered table loads");
1377 assert_eq!(schema.table, table);
1378 });
1379 handles.push(handle);
1380 }
1381 for handle in handles {
1382 handle.await.unwrap();
1383 }
1384
1385 let stats = manager
1387 .registry()
1388 .read()
1389 .await
1390 .get_statistics()
1391 .await
1392 .unwrap();
1393 assert_eq!(stats.total_schemas, 3);
1394 for i in 0..3 {
1395 assert!(manager.load_schema(&format!("table_{}", i)).await.is_ok());
1396 }
1397 }
1398
1399 #[tokio::test]
1405 async fn test_manager_reflects_registry_updates_no_stale_snapshot() {
1406 let registry = build_shared_registry().await;
1407
1408 let v1 = table_schema_named("evolving");
1410 assert_eq!(v1.columns.len(), 2);
1411 registry
1412 .read()
1413 .await
1414 .register_schema(v1.clone(), registry::SchemaSource::Manual)
1415 .await
1416 .unwrap();
1417
1418 let storage = build_storage().await;
1419 let config = Config::default();
1420 let manager = SchemaManager::new_with_registry(storage, registry.clone(), &config)
1421 .await
1422 .unwrap();
1423
1424 let got_v1 = manager.get_table_schema("evolving").await.unwrap();
1425 assert_eq!(got_v1.columns.len(), 2, "manager must see v1");
1426
1427 let mut v2 = v1.clone();
1429 v2.columns.push(Column {
1430 name: "extra".to_string(),
1431 data_type: "int".to_string(),
1432 nullable: true,
1433 default: None,
1434 is_static: false,
1435 });
1436 registry
1437 .read()
1438 .await
1439 .register_schema(v2, registry::SchemaSource::Manual)
1440 .await
1441 .unwrap();
1442
1443 let got_v2 = manager.get_table_schema("evolving").await.unwrap();
1444 assert_eq!(
1445 got_v2.columns.len(),
1446 3,
1447 "manager must resolve THROUGH the registry, not a stale by-value snapshot"
1448 );
1449 }
1450
1451 #[tokio::test]
1459 async fn test_manager_does_not_serve_ttl_expired_schema() {
1460 let config = Config::default();
1461 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1462 let mut reg_config = registry::SchemaRegistryConfig::default();
1463 reg_config.cache_ttl_seconds = 0; let registry = Arc::new(RwLock::new(
1465 registry::SchemaRegistry::new(reg_config, platform, config.clone())
1466 .await
1467 .unwrap(),
1468 ));
1469
1470 registry
1472 .read()
1473 .await
1474 .register_schema(
1475 table_schema_named("stale_tbl"),
1476 registry::SchemaSource::Manual,
1477 )
1478 .await
1479 .unwrap();
1480
1481 let storage = build_storage().await;
1482 let manager = SchemaManager::new_with_registry(storage, registry.clone(), &config)
1483 .await
1484 .unwrap();
1485
1486 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1488
1489 let loaded = manager.load_schema("stale_tbl").await;
1493 assert!(
1494 loaded.is_err(),
1495 "manager must delegate TTL freshness to the registry, not serve a stale schema; got {loaded:?}"
1496 );
1497 let got = manager.get_table_schema("stale_tbl").await;
1498 assert!(
1499 got.is_err(),
1500 "get_table_schema must also honor registry expiry; got {got:?}"
1501 );
1502
1503 assert!(
1505 manager.load_schema("never_defined_here").await.is_err(),
1506 "unknown table still errors (no fabrication)"
1507 );
1508 }
1509
1510 #[tokio::test]
1516 async fn test_all_constructors_share_udt_registry() {
1517 let temp = tempfile::tempdir().unwrap();
1519 let via_new = SchemaManager::new(temp.path()).await.unwrap();
1520
1521 let config = Config::default();
1523 let via_storage = {
1524 let storage = build_storage().await;
1525 SchemaManager::new_with_storage(storage, &config)
1526 .await
1527 .unwrap()
1528 };
1529
1530 let via_registry = {
1532 let registry = build_shared_registry().await;
1533 let storage = build_storage().await;
1534 SchemaManager::new_with_registry(storage, registry, &config)
1535 .await
1536 .unwrap()
1537 };
1538
1539 for (label, manager) in [
1540 ("new", &via_new),
1541 ("new_with_storage", &via_storage),
1542 ("new_with_registry", &via_registry),
1543 ] {
1544 let point = UdtTypeDef::new("ks_share".to_string(), "point".to_string()).with_field(
1546 "x".to_string(),
1547 CqlType::Int,
1548 true,
1549 );
1550 manager
1551 .registry()
1552 .read()
1553 .await
1554 .register_udt(point)
1555 .await
1556 .unwrap();
1557 assert!(
1558 manager.get_udt("ks_share", "point").await.is_some(),
1559 "[{label}] UDT registered in the shared registry must be visible via the manager"
1560 );
1561
1562 let line = UdtTypeDef::new("ks_share".to_string(), "line".to_string()).with_field(
1564 "len".to_string(),
1565 CqlType::Int,
1566 true,
1567 );
1568 manager.register_udt(line).await;
1569 let seen = manager
1570 .registry()
1571 .read()
1572 .await
1573 .get_udt("ks_share", "line")
1574 .await
1575 .unwrap();
1576 assert!(
1577 seen.is_some(),
1578 "[{label}] UDT registered via the manager must be visible in the shared registry"
1579 );
1580 }
1581 }
1582
1583 #[test]
1584 fn test_schema_from_sstable_header() {
1585 use crate::parser::header::{
1586 CassandraVersion, ColumnInfo, CompressionInfo, SSTableHeader, SSTableStats,
1587 };
1588 use std::collections::HashMap;
1589
1590 let columns = vec![
1591 ColumnInfo {
1592 name: "id".to_string(),
1593 column_type: "int".to_string(),
1594 is_primary_key: true,
1595 key_position: Some(0),
1596 is_static: false,
1597 is_clustering: false,
1598 clustering_reversed: false,
1599 },
1600 ColumnInfo {
1601 name: "name".to_string(),
1602 column_type: "text".to_string(),
1603 is_primary_key: false,
1604 key_position: None,
1605 is_static: false,
1606 is_clustering: false,
1607 clustering_reversed: false,
1608 },
1609 ];
1610
1611 let header = SSTableHeader {
1612 cassandra_version: CassandraVersion::V5_0Bti,
1613 version: 1,
1614 table_id: [0; 16],
1615 keyspace: "test_ks".to_string(),
1616 table_name: "test_table".to_string(),
1617 generation: 1,
1618 compression: CompressionInfo {
1619 algorithm: "NONE".to_string(),
1620 chunk_size: 0,
1621 parameters: HashMap::new(),
1622 },
1623 stats: SSTableStats::default(),
1624 columns,
1625 properties: HashMap::new(),
1626 };
1627
1628 let schema = TableSchema::from_sstable_header(&header).unwrap();
1629
1630 assert_eq!(schema.keyspace, "test_ks");
1631 assert_eq!(schema.table, "test_table");
1632 assert_eq!(schema.partition_keys.len(), 1);
1633 assert_eq!(schema.partition_keys[0].name, "id");
1634 assert_eq!(schema.columns.len(), 2);
1635 }
1636}