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::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) = match udt_name.split_once('.') {
698 Some((ks, name)) => (ks, name),
699 None => (self.keyspace.as_str(), udt_name),
700 };
701
702 if registry.contains_udt(lookup_keyspace, bare_name)
703 || registry.contains_udt("system", bare_name)
704 {
705 return Ok(());
706 }
707
708 Err(Error::schema(format!(
709 "Column '{}' references undefined UDT '{}' in keyspace '{}'",
710 column_name, udt_name, lookup_keyspace
711 )))
712 }
713
714 pub fn get_column(&self, name: &str) -> Option<&Column> {
716 self.columns.iter().find(|c| c.name == name)
717 }
718
719 pub fn is_partition_key(&self, name: &str) -> bool {
721 self.partition_keys.iter().any(|k| k.name == name)
722 }
723
724 pub fn is_clustering_key(&self, name: &str) -> bool {
726 self.clustering_keys.iter().any(|k| k.name == name)
727 }
728
729 #[cfg(test)]
735 pub fn new_for_testing(keyspace: &str, table: &str) -> Self {
736 Self {
737 keyspace: keyspace.to_string(),
738 table: table.to_string(),
739 partition_keys: vec![KeyColumn {
740 name: "id".to_string(),
741 data_type: "int".to_string(),
742 position: 0,
743 }],
744 clustering_keys: vec![],
745 columns: vec![Column {
746 name: "id".to_string(),
747 data_type: "int".to_string(),
748 nullable: false,
749 default: None,
750 is_static: false,
751 }],
752 comments: HashMap::new(),
753 dropped_columns: HashMap::new(),
754 }
755 }
756}
757
758#[derive(Debug)]
768pub struct SchemaManager {
769 #[allow(dead_code)]
770 storage: Arc<StorageEngine>,
771 registry: Arc<RwLock<registry::SchemaRegistry>>,
773}
774
775impl SchemaManager {
776 async fn default_registry(
778 platform: Arc<crate::platform::Platform>,
779 config: &Config,
780 ) -> Result<Arc<RwLock<registry::SchemaRegistry>>> {
781 let registry = registry::SchemaRegistry::new(
782 registry::SchemaRegistryConfig::default(),
783 platform,
784 config.clone(),
785 )
786 .await?;
787 Ok(Arc::new(RwLock::new(registry)))
788 }
789
790 pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
792 let config = Config::default();
794 let platform = Arc::new(crate::platform::Platform::new(&config).await?);
795 let storage = Arc::new(
796 StorageEngine::open(
797 path.as_ref(),
798 &config,
799 platform.clone(),
800 #[cfg(feature = "state_machine")]
801 None,
802 )
803 .await?,
804 );
805
806 let registry = Self::default_registry(platform, &config).await?;
807 Ok(Self { storage, registry })
808 }
809
810 pub async fn new_with_storage(storage: Arc<StorageEngine>, config: &Config) -> Result<Self> {
812 let platform = Arc::new(crate::platform::Platform::new(config).await?);
813 let registry = Self::default_registry(platform, config).await?;
814 let manager = Self { storage, registry };
815
816 manager.load_default_udts().await;
818
819 Ok(manager)
820 }
821
822 pub async fn new_with_registry(
837 storage: Arc<StorageEngine>,
838 registry: Arc<tokio::sync::RwLock<registry::SchemaRegistry>>,
839 _config: &Config,
840 ) -> Result<Self> {
841 Ok(Self { storage, registry })
842 }
843
844 #[cfg(test)]
849 pub(crate) fn registry(&self) -> Arc<RwLock<registry::SchemaRegistry>> {
850 self.registry.clone()
851 }
852
853 async fn load_default_udts(&self) {
855 let address_udt = UdtTypeDef::new("test_keyspace".to_string(), "address".to_string())
857 .with_field("street".to_string(), CqlType::Text, true)
858 .with_field("city".to_string(), CqlType::Text, true)
859 .with_field("state".to_string(), CqlType::Text, true)
860 .with_field("zip_code".to_string(), CqlType::Text, true)
861 .with_field("country".to_string(), CqlType::Text, true);
862
863 self.register_udt(address_udt).await;
864
865 let person_udt = UdtTypeDef::new("test_keyspace".to_string(), "person".to_string())
867 .with_field("name".to_string(), CqlType::Text, true)
868 .with_field("age".to_string(), CqlType::Int, true)
869 .with_field("email".to_string(), CqlType::Text, true)
870 .with_field(
871 "addresses".to_string(),
872 CqlType::List(Box::new(CqlType::Udt(
873 "address".to_string(),
874 vec![
875 ("street".to_string(), CqlType::Text),
876 ("city".to_string(), CqlType::Text),
877 ("state".to_string(), CqlType::Text),
878 ("zip_code".to_string(), CqlType::Text),
879 ("country".to_string(), CqlType::Text),
880 ],
881 ))),
882 true,
883 )
884 .with_field(
885 "contact_info".to_string(),
886 CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::Text)),
887 true,
888 );
889
890 self.register_udt(person_udt).await;
891
892 let company_udt = UdtTypeDef::new("test_keyspace".to_string(), "company".to_string())
894 .with_field("name".to_string(), CqlType::Text, false)
895 .with_field(
896 "headquarters".to_string(),
897 CqlType::Udt(
898 "address".to_string(),
899 vec![
900 ("street".to_string(), CqlType::Text),
901 ("city".to_string(), CqlType::Text),
902 ("state".to_string(), CqlType::Text),
903 ("zip_code".to_string(), CqlType::Text),
904 ("country".to_string(), CqlType::Text),
905 ],
906 ),
907 true,
908 )
909 .with_field(
910 "employees".to_string(),
911 CqlType::Set(Box::new(CqlType::Udt("person".to_string(), vec![]))),
912 true,
913 )
914 .with_field("founded_year".to_string(), CqlType::Int, true);
915
916 self.register_udt(company_udt).await;
917 }
918
919 pub async fn register_udt(&self, udt_def: UdtTypeDef) {
921 let _ = self.registry.read().await.register_udt(udt_def).await;
924 }
925
926 pub async fn get_udt(&self, keyspace: &str, name: &str) -> Option<UdtTypeDef> {
928 self.registry
929 .read()
930 .await
931 .get_udt(keyspace, name)
932 .await
933 .ok()
934 .flatten()
935 }
936
937 pub async fn load_schema(&self, table_name: &str) -> Result<TableSchema> {
949 let (keyspace, table) = match table_name.split_once('.') {
952 Some((ks, tbl)) => (Some(ks.to_string()), tbl),
953 None => (None, table_name),
954 };
955 self.find_schema_by_table(&keyspace, table)
956 .await?
957 .ok_or_else(|| {
958 Error::schema(format!(
959 "unknown table {}; no schema registered or discovered",
960 table_name
961 ))
962 })
963 }
964
965 pub async fn parse_and_register_cql_schema(&self, cql: &str) -> Result<TableSchema> {
971 let schema = cql_parser::parse_cql_schema(cql)?;
972 self.registry
973 .read()
974 .await
975 .register_schema(schema.clone(), registry::SchemaSource::Cql(cql.to_string()))
976 .await?;
977 Ok(schema)
978 }
979
980 pub async fn find_schema_by_table(
988 &self,
989 keyspace: &Option<String>,
990 table: &str,
991 ) -> Result<Option<TableSchema>> {
992 let found = self
993 .registry
994 .read()
995 .await
996 .find_schema_by_table(keyspace, table)
997 .await?;
998 #[cfg(test)]
1001 if found.is_some() {
1002 TABLE_SCHEMA_CLONES.with(|c| c.set(c.get() + 1));
1003 }
1004 Ok(found)
1005 }
1006
1007 pub fn extract_table_info(&self, cql: &str) -> Result<(Option<String>, String)> {
1009 cql_parser::extract_table_name(cql)
1010 }
1011
1012 pub fn cql_type_to_internal(&self, cql_type: &str) -> Result<CqlTypeId> {
1014 cql_parser::cql_type_to_type_id(cql_type)
1015 }
1016
1017 pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema> {
1019 if let Some(schema) = self.find_schema_by_table(&None, table_name).await? {
1021 Ok(schema)
1022 } else {
1023 Err(Error::Schema(format!(
1024 "Table schema not found: {}",
1025 table_name
1026 )))
1027 }
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 #[test]
1036 fn test_schema_validation() {
1037 let schema_json = r#"
1038 {
1039 "keyspace": "test",
1040 "table": "users",
1041 "partition_keys": [
1042 {"name": "id", "type": "bigint", "position": 0}
1043 ],
1044 "clustering_keys": [],
1045 "columns": [
1046 {"name": "id", "type": "bigint", "nullable": false},
1047 {"name": "name", "type": "text", "nullable": true}
1048 ]
1049 }
1050 "#;
1051
1052 let schema = TableSchema::from_json(schema_json).unwrap();
1053 assert_eq!(schema.keyspace, "test");
1054 assert_eq!(schema.table, "users");
1055 assert_eq!(schema.partition_keys.len(), 1);
1056 assert_eq!(schema.columns.len(), 2);
1057 }
1058
1059 #[test]
1060 fn test_schema_validation_failures() {
1061 let invalid_schema = r#"
1063 {
1064 "keyspace": "test",
1065 "table": "users",
1066 "partition_keys": [],
1067 "clustering_keys": [],
1068 "columns": []
1069 }
1070 "#;
1071
1072 assert!(TableSchema::from_json(invalid_schema).is_err());
1073
1074 let invalid_type = r#"
1076 {
1077 "keyspace": "test",
1078 "table": "users",
1079 "partition_keys": [
1080 {"name": "id", "type": "invalid_type", "position": 0}
1081 ],
1082 "clustering_keys": [],
1083 "columns": [
1084 {"name": "id", "type": "invalid_type", "nullable": false}
1085 ]
1086 }
1087 "#;
1088
1089 assert!(TableSchema::from_json(invalid_type).is_ok());
1091 }
1092
1093 fn udt_schema(column_type: &str) -> TableSchema {
1094 TableSchema {
1095 keyspace: "test_ks".to_string(),
1096 table: "t".to_string(),
1097 partition_keys: vec![KeyColumn {
1098 name: "id".to_string(),
1099 data_type: "uuid".to_string(),
1100 position: 0,
1101 }],
1102 clustering_keys: vec![],
1103 columns: vec![
1104 Column {
1105 name: "id".to_string(),
1106 data_type: "uuid".to_string(),
1107 nullable: false,
1108 default: None,
1109 is_static: false,
1110 },
1111 Column {
1112 name: "value".to_string(),
1113 data_type: column_type.to_string(),
1114 nullable: true,
1115 default: None,
1116 is_static: false,
1117 },
1118 ],
1119 comments: HashMap::new(),
1120 dropped_columns: HashMap::new(),
1121 }
1122 }
1123
1124 #[test]
1125 fn test_udt_reference_undefined_top_level_errors() {
1126 let registry = UdtRegistry::new();
1127 let schema = udt_schema("MyMissingType");
1128
1129 let err = schema
1130 .validate_udt_references(®istry)
1131 .expect_err("undefined UDT reference must fail validation");
1132 let msg = err.to_string();
1133 assert!(
1134 matches!(err, Error::Schema(_)),
1135 "expected schema-category error, got {err:?}"
1136 );
1137 assert!(
1138 msg.contains("MyMissingType"),
1139 "error must name the missing UDT, got: {msg}"
1140 );
1141 }
1142
1143 #[test]
1144 fn test_udt_reference_undefined_lowercase_errors() {
1145 let registry = UdtRegistry::new();
1150 for col_type in ["address", "list<frozen<address>>"] {
1151 let schema = udt_schema(col_type);
1152 let err = schema
1153 .validate_udt_references(®istry)
1154 .expect_err("undefined lowercase UDT must fail validation");
1155 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1156 assert!(
1157 err.to_string().contains("address"),
1158 "error must name the missing UDT, got: {err}"
1159 );
1160 }
1161 }
1162
1163 #[test]
1164 fn test_uppercase_collection_of_primitives_is_not_a_udt() {
1165 let registry = UdtRegistry::new();
1168 for col_type in [
1169 "SET<TEXT>",
1170 "LIST<INT>",
1171 "MAP<TEXT, TEXT>",
1172 "FROZEN<LIST<INT>>",
1173 ] {
1174 let schema = udt_schema(col_type);
1175 schema
1176 .validate_udt_references(®istry)
1177 .unwrap_or_else(|e| panic!("'{col_type}' must not be flagged as a UDT: {e}"));
1178 }
1179 }
1180
1181 #[test]
1182 fn test_uppercase_collection_with_undefined_udt_errors() {
1183 let registry = UdtRegistry::new();
1187 for col_type in [
1188 "LIST<MissingType>",
1189 "MAP<TEXT, MissingType>",
1190 "FROZEN<SET<MissingType>>",
1191 ] {
1192 let schema = udt_schema(col_type);
1193 let err = schema
1194 .validate_udt_references(®istry)
1195 .expect_err("undefined UDT in uppercase collection must fail");
1196 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1197 assert!(
1198 err.to_string().contains("MissingType"),
1199 "error must name the missing UDT, got: {err}"
1200 );
1201 }
1202 }
1203
1204 #[test]
1205 fn test_udt_reference_undefined_nested_in_collection_errors() {
1206 let registry = UdtRegistry::new();
1207 let schema = udt_schema("list<frozen<NestedMissing>>");
1208
1209 let err = schema
1210 .validate_udt_references(®istry)
1211 .expect_err("nested undefined UDT reference must fail validation");
1212 let msg = err.to_string();
1213 assert!(matches!(err, Error::Schema(_)));
1214 assert!(
1215 msg.contains("NestedMissing"),
1216 "error must name the nested missing UDT, got: {msg}"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_udt_reference_undefined_nested_in_map_errors() {
1222 let registry = UdtRegistry::new();
1223 let schema = udt_schema("map<text, MapValueMissing>");
1224
1225 let err = schema
1226 .validate_udt_references(®istry)
1227 .expect_err("undefined UDT in map value must fail validation");
1228 assert!(matches!(err, Error::Schema(_)));
1229 assert!(err.to_string().contains("MapValueMissing"));
1230 }
1231
1232 #[test]
1233 fn test_udt_reference_defined_top_level_ok() {
1234 let mut registry = UdtRegistry::new();
1235 registry.register_udt(
1236 UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1237 "a".to_string(),
1238 CqlType::Text,
1239 true,
1240 ),
1241 );
1242 let schema = udt_schema("MyType");
1243 schema
1244 .validate_udt_references(®istry)
1245 .expect("defined UDT should validate");
1246 }
1247
1248 #[test]
1249 fn test_udt_reference_defined_nested_ok() {
1250 let mut registry = UdtRegistry::new();
1251 registry.register_udt(
1252 UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1253 "a".to_string(),
1254 CqlType::Text,
1255 true,
1256 ),
1257 );
1258 let schema = udt_schema("list<frozen<MyType>>");
1259 schema
1260 .validate_udt_references(®istry)
1261 .expect("defined nested UDT should validate");
1262 }
1263
1264 #[test]
1265 fn test_validate_udt_references_no_udts_ok() {
1266 let registry = UdtRegistry::new();
1268 let schema = udt_schema("map<text, list<int>>");
1269 schema
1270 .validate_udt_references(®istry)
1271 .expect("primitive/collection-only schema should validate");
1272 }
1273
1274 async fn build_storage() -> Arc<StorageEngine> {
1275 let config = Config::default();
1276 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1277 let temp_dir = tempfile::tempdir().unwrap();
1278 Arc::new(
1279 StorageEngine::open(
1280 temp_dir.path(),
1281 &config,
1282 platform,
1283 #[cfg(feature = "state_machine")]
1284 None,
1285 )
1286 .await
1287 .unwrap(),
1288 )
1289 }
1290
1291 async fn build_shared_registry() -> Arc<RwLock<registry::SchemaRegistry>> {
1292 let config = Config::default();
1293 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1294 Arc::new(RwLock::new(
1295 registry::SchemaRegistry::new(
1296 registry::SchemaRegistryConfig::default(),
1297 platform,
1298 config,
1299 )
1300 .await
1301 .unwrap(),
1302 ))
1303 }
1304
1305 async fn test_manager() -> Arc<SchemaManager> {
1306 let config = Config::default();
1307 let storage = build_storage().await;
1308 Arc::new(
1309 SchemaManager::new_with_storage(storage, &config)
1310 .await
1311 .unwrap(),
1312 )
1313 }
1314
1315 fn table_schema_named(table_name: &str) -> TableSchema {
1316 let mut schema = udt_schema("text");
1317 schema.table = table_name.to_string();
1318 schema
1319 }
1320
1321 #[tokio::test]
1325 async fn test_load_schema_unknown_table_errs() {
1326 let manager = test_manager().await;
1327
1328 let err = manager
1329 .load_schema("never_defined_table")
1330 .await
1331 .expect_err("unknown table must error, not fabricate a schema");
1332 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1333 assert!(
1334 err.to_string().contains("never_defined_table"),
1335 "error must name the unknown table, got: {err}"
1336 );
1337
1338 let stats = manager
1340 .registry()
1341 .read()
1342 .await
1343 .get_statistics()
1344 .await
1345 .unwrap();
1346 assert_eq!(
1347 stats.total_schemas, 0,
1348 "unknown-table lookup must not mutate the registry"
1349 );
1350 }
1351
1352 #[tokio::test]
1355 async fn test_concurrent_schema_access() {
1356 let manager = test_manager().await;
1357
1358 for i in 0..3 {
1361 let name = format!("table_{}", i);
1362 manager
1363 .registry()
1364 .read()
1365 .await
1366 .register_schema(table_schema_named(&name), registry::SchemaSource::Manual)
1367 .await
1368 .unwrap();
1369 }
1370
1371 let mut handles = vec![];
1373 for i in 0..10 {
1374 let m = Arc::clone(&manager);
1375 let handle = tokio::spawn(async move {
1376 let table = format!("table_{}", i % 3);
1377 let schema = m.load_schema(&table).await.expect("registered table loads");
1378 assert_eq!(schema.table, table);
1379 });
1380 handles.push(handle);
1381 }
1382 for handle in handles {
1383 handle.await.unwrap();
1384 }
1385
1386 let stats = manager
1388 .registry()
1389 .read()
1390 .await
1391 .get_statistics()
1392 .await
1393 .unwrap();
1394 assert_eq!(stats.total_schemas, 3);
1395 for i in 0..3 {
1396 assert!(manager.load_schema(&format!("table_{}", i)).await.is_ok());
1397 }
1398 }
1399
1400 #[tokio::test]
1406 async fn test_manager_reflects_registry_updates_no_stale_snapshot() {
1407 let registry = build_shared_registry().await;
1408
1409 let v1 = table_schema_named("evolving");
1411 assert_eq!(v1.columns.len(), 2);
1412 registry
1413 .read()
1414 .await
1415 .register_schema(v1.clone(), registry::SchemaSource::Manual)
1416 .await
1417 .unwrap();
1418
1419 let storage = build_storage().await;
1420 let config = Config::default();
1421 let manager = SchemaManager::new_with_registry(storage, registry.clone(), &config)
1422 .await
1423 .unwrap();
1424
1425 let got_v1 = manager.get_table_schema("evolving").await.unwrap();
1426 assert_eq!(got_v1.columns.len(), 2, "manager must see v1");
1427
1428 let mut v2 = v1.clone();
1430 v2.columns.push(Column {
1431 name: "extra".to_string(),
1432 data_type: "int".to_string(),
1433 nullable: true,
1434 default: None,
1435 is_static: false,
1436 });
1437 registry
1438 .read()
1439 .await
1440 .register_schema(v2, registry::SchemaSource::Manual)
1441 .await
1442 .unwrap();
1443
1444 let got_v2 = manager.get_table_schema("evolving").await.unwrap();
1445 assert_eq!(
1446 got_v2.columns.len(),
1447 3,
1448 "manager must resolve THROUGH the registry, not a stale by-value snapshot"
1449 );
1450 }
1451
1452 #[tokio::test]
1460 async fn test_manager_does_not_serve_ttl_expired_schema() {
1461 let config = Config::default();
1462 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1463 let mut reg_config = registry::SchemaRegistryConfig::default();
1464 reg_config.cache_ttl_seconds = 0; let registry = Arc::new(RwLock::new(
1466 registry::SchemaRegistry::new(reg_config, platform, config.clone())
1467 .await
1468 .unwrap(),
1469 ));
1470
1471 registry
1473 .read()
1474 .await
1475 .register_schema(
1476 table_schema_named("stale_tbl"),
1477 registry::SchemaSource::Manual,
1478 )
1479 .await
1480 .unwrap();
1481
1482 let storage = build_storage().await;
1483 let manager = SchemaManager::new_with_registry(storage, registry.clone(), &config)
1484 .await
1485 .unwrap();
1486
1487 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1489
1490 let loaded = manager.load_schema("stale_tbl").await;
1494 assert!(
1495 loaded.is_err(),
1496 "manager must delegate TTL freshness to the registry, not serve a stale schema; got {loaded:?}"
1497 );
1498 let got = manager.get_table_schema("stale_tbl").await;
1499 assert!(
1500 got.is_err(),
1501 "get_table_schema must also honor registry expiry; got {got:?}"
1502 );
1503
1504 assert!(
1506 manager.load_schema("never_defined_here").await.is_err(),
1507 "unknown table still errors (no fabrication)"
1508 );
1509 }
1510
1511 #[tokio::test]
1517 async fn test_all_constructors_share_udt_registry() {
1518 let temp = tempfile::tempdir().unwrap();
1520 let via_new = SchemaManager::new(temp.path()).await.unwrap();
1521
1522 let config = Config::default();
1524 let via_storage = {
1525 let storage = build_storage().await;
1526 SchemaManager::new_with_storage(storage, &config)
1527 .await
1528 .unwrap()
1529 };
1530
1531 let via_registry = {
1533 let registry = build_shared_registry().await;
1534 let storage = build_storage().await;
1535 SchemaManager::new_with_registry(storage, registry, &config)
1536 .await
1537 .unwrap()
1538 };
1539
1540 for (label, manager) in [
1541 ("new", &via_new),
1542 ("new_with_storage", &via_storage),
1543 ("new_with_registry", &via_registry),
1544 ] {
1545 let point = UdtTypeDef::new("ks_share".to_string(), "point".to_string()).with_field(
1547 "x".to_string(),
1548 CqlType::Int,
1549 true,
1550 );
1551 manager
1552 .registry()
1553 .read()
1554 .await
1555 .register_udt(point)
1556 .await
1557 .unwrap();
1558 assert!(
1559 manager.get_udt("ks_share", "point").await.is_some(),
1560 "[{label}] UDT registered in the shared registry must be visible via the manager"
1561 );
1562
1563 let line = UdtTypeDef::new("ks_share".to_string(), "line".to_string()).with_field(
1565 "len".to_string(),
1566 CqlType::Int,
1567 true,
1568 );
1569 manager.register_udt(line).await;
1570 let seen = manager
1571 .registry()
1572 .read()
1573 .await
1574 .get_udt("ks_share", "line")
1575 .await
1576 .unwrap();
1577 assert!(
1578 seen.is_some(),
1579 "[{label}] UDT registered via the manager must be visible in the shared registry"
1580 );
1581 }
1582 }
1583
1584 #[test]
1585 fn test_schema_from_sstable_header() {
1586 use crate::parser::header::{
1587 CassandraVersion, ColumnInfo, CompressionInfo, SSTableHeader, SSTableStats,
1588 };
1589 use std::collections::HashMap;
1590
1591 let columns = vec![
1592 ColumnInfo {
1593 name: "id".to_string(),
1594 column_type: "int".to_string(),
1595 is_primary_key: true,
1596 key_position: Some(0),
1597 is_static: false,
1598 is_clustering: false,
1599 clustering_reversed: false,
1600 },
1601 ColumnInfo {
1602 name: "name".to_string(),
1603 column_type: "text".to_string(),
1604 is_primary_key: false,
1605 key_position: None,
1606 is_static: false,
1607 is_clustering: false,
1608 clustering_reversed: false,
1609 },
1610 ];
1611
1612 let header = SSTableHeader {
1613 cassandra_version: CassandraVersion::V5_0Bti,
1614 version: 1,
1615 table_id: [0; 16],
1616 keyspace: "test_ks".to_string(),
1617 table_name: "test_table".to_string(),
1618 generation: 1,
1619 compression: CompressionInfo {
1620 algorithm: "NONE".to_string(),
1621 chunk_size: 0,
1622 parameters: HashMap::new(),
1623 },
1624 stats: SSTableStats::default(),
1625 columns,
1626 properties: HashMap::new(),
1627 };
1628
1629 let schema = TableSchema::from_sstable_header(&header).unwrap();
1630
1631 assert_eq!(schema.keyspace, "test_ks");
1632 assert_eq!(schema.table, "test_table");
1633 assert_eq!(schema.partition_keys.len(), 1);
1634 assert_eq!(schema.partition_keys[0].name, "id");
1635 assert_eq!(schema.columns.len(), 2);
1636 }
1637}