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 schema_comparator;
21mod udt_registry;
22
23pub use udt_registry::UdtRegistry;
24
25#[cfg(test)]
38pub(crate) mod work_counters {
39 use std::cell::Cell;
40
41 thread_local! {
42 static PARSE_CALLS: Cell<usize> = const { Cell::new(0) };
44 static SCHEMA_CLONES: Cell<usize> = const { Cell::new(0) };
47 }
48
49 pub(crate) fn record_parse_call() {
50 PARSE_CALLS.with(|c| c.set(c.get() + 1));
51 }
52
53 pub(crate) fn record_schema_clone() {
54 SCHEMA_CLONES.with(|c| c.set(c.get() + 1));
55 }
56
57 pub(crate) fn reset() {
59 PARSE_CALLS.with(|c| c.set(0));
60 SCHEMA_CLONES.with(|c| c.set(0));
61 }
62
63 pub(crate) fn parse_calls() -> usize {
64 PARSE_CALLS.with(|c| c.get())
65 }
66
67 pub(crate) fn schema_clones() -> usize {
68 SCHEMA_CLONES.with(|c| c.get())
69 }
70}
71
72pub use aggregator::{
74 AggregatorConfig, LoadErrorType, LoadResult, SchemaAggregator, SchemaLoadError,
75 SchemaLoadWarning,
76};
77
78pub use cql_parser::{
80 cql_type_to_type_id, extract_table_name, parse_cql_schema, parse_cql_schema_with_visitor,
81 parse_create_table, table_name_matches,
82};
83
84pub use discovery::{
86 ColumnDefinition, DiscoveryMethod, IndexDefinition, SchemaDiscoveryConfig,
87 SchemaDiscoveryEngine, SchemaInfo, SchemaMetadata, TableOptions, TypeInfo, UDTDefinition,
88 ValidationError, ValidationResults, ValidationStatus, ValidationWarning,
89};
90
91pub use registry::{
92 ParsingContext, RegistryStatistics, SchemaChange, SchemaChangeType, SchemaQuery,
93 SchemaRegistry, SchemaRegistryConfig, SchemaSource, SchemaValidationStatus, SchemaValidator,
94 SchemaVersion, ValidationReport,
95};
96
97#[cfg(test)]
103thread_local! {
104 pub(crate) static TABLE_SCHEMA_CLONES: std::cell::Cell<usize> =
105 const { std::cell::Cell::new(0) };
106}
107
108pub use parser::SchemaParser;
109
110#[cfg(feature = "experimental")]
111pub use json_exporter::{
112 JsonClusteringKey, JsonColumn, JsonExportConfig, JsonExporter, JsonFormat, JsonIndex,
113 JsonMetadata, JsonPerformanceMetrics, JsonPrimaryKey, JsonSchema, JsonTable, JsonTableOptions,
114 JsonUDT, JsonValidationResults,
115};
116
117pub type ColumnSpec = Column;
119
120use crate::error::{Error, Result};
121use crate::parser::header::SSTableHeader;
122use crate::parser::types::CqlTypeId;
123use crate::storage::StorageEngine;
124use crate::types::UdtTypeDef;
125use crate::Config;
126use serde::{Deserialize, Serialize};
127use std::collections::HashMap;
128use std::fs;
129use std::path::Path;
130use std::sync::Arc;
131use tokio::sync::RwLock;
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct TableSchema {
136 pub keyspace: String,
138
139 pub table: String,
141
142 pub partition_keys: Vec<KeyColumn>,
144
145 pub clustering_keys: Vec<ClusteringColumn>,
147
148 pub columns: Vec<Column>,
150
151 #[serde(default)]
153 pub comments: HashMap<String, String>,
154
155 #[serde(default)]
179 pub dropped_columns: HashMap<String, i64>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct KeyColumn {
185 pub name: String,
187
188 #[serde(rename = "type")]
190 pub data_type: String,
191
192 pub position: usize,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ClusteringColumn {
199 pub name: String,
201
202 #[serde(rename = "type")]
204 pub data_type: String,
205
206 pub position: usize,
208
209 #[serde(default)]
211 pub order: ClusteringOrder,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
216pub enum ClusteringOrder {
217 #[default]
219 Asc,
220 Desc,
222}
223
224impl From<&str> for ClusteringOrder {
225 fn from(s: &str) -> Self {
226 match s.to_uppercase().as_str() {
227 "DESC" => ClusteringOrder::Desc,
228 _ => ClusteringOrder::Asc,
229 }
230 }
231}
232
233impl std::fmt::Display for ClusteringOrder {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 match self {
236 ClusteringOrder::Asc => write!(f, "ASC"),
237 ClusteringOrder::Desc => write!(f, "DESC"),
238 }
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct Column {
245 pub name: String,
247
248 #[serde(rename = "type")]
250 pub data_type: String,
251
252 #[serde(default)]
254 pub nullable: bool,
255
256 #[serde(default)]
258 pub default: Option<serde_json::Value>,
259
260 #[serde(default)]
262 pub is_static: bool,
263}
264
265#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
267pub enum CqlType {
268 Boolean,
270 TinyInt,
271 SmallInt,
272 Int,
273 BigInt,
274 Counter,
275 Float,
276 Double,
277 Decimal,
278 Text,
279 Ascii,
280 Varchar,
281 Blob,
282 Timestamp,
283 Date,
284 Time,
285 Uuid,
286 TimeUuid,
287 Inet,
288 Duration,
289 Varint,
290
291 List(Box<CqlType>),
293 Set(Box<CqlType>),
294 Map(Box<CqlType>, Box<CqlType>),
295
296 Tuple(Vec<CqlType>),
298 Udt(String, Vec<(String, CqlType)>), Frozen(Box<CqlType>),
300
301 Custom(String),
303}
304
305pub(crate) fn is_udt_identifier(name: &str) -> bool {
313 !name.is_empty()
314 && name
315 .chars()
316 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
317}
318
319impl TableSchema {
320 pub fn from_sstable_header(header: &SSTableHeader) -> Result<Self> {
325 let mut partition_keys = Vec::new();
327 let mut clustering_keys = Vec::new();
328 let mut regular_columns = Vec::new();
329
330 for col_info in &header.columns {
331 if col_info.is_primary_key {
332 if col_info.is_clustering {
333 clustering_keys.push(col_info);
334 } else {
335 partition_keys.push(col_info);
336 }
337 } else {
338 regular_columns.push(col_info);
339 }
340 }
341
342 for col_info in &partition_keys {
344 if col_info.key_position.is_none() {
345 return Err(Error::schema(format!(
346 "Partition key column '{}' missing key_position in SSTable header",
347 col_info.name
348 )));
349 }
350 }
351
352 for col_info in &clustering_keys {
354 if col_info.key_position.is_none() {
355 return Err(Error::schema(format!(
356 "Clustering key column '{}' missing key_position in SSTable header",
357 col_info.name
358 )));
359 }
360 }
361
362 partition_keys.sort_by_key(|c| c.key_position.unwrap());
364 clustering_keys.sort_by_key(|c| c.key_position.unwrap());
365
366 let partition_keys: Vec<KeyColumn> = partition_keys
369 .iter()
370 .enumerate()
371 .map(|(pos, col)| KeyColumn {
372 name: col.name.clone(),
373 data_type: col.column_type.clone(),
374 position: pos, })
376 .collect();
377
378 let clustering_keys: Vec<ClusteringColumn> = clustering_keys
380 .iter()
381 .enumerate()
382 .map(|(pos, col)| ClusteringColumn {
383 name: col.name.clone(),
384 data_type: col.column_type.clone(),
385 position: pos, order: if col.clustering_reversed {
392 ClusteringOrder::Desc
393 } else {
394 ClusteringOrder::Asc
395 },
396 })
397 .collect();
398
399 let columns: Vec<Column> = header
401 .columns
402 .iter()
403 .map(|col| Column {
404 name: col.name.clone(),
405 data_type: col.column_type.clone(),
406 nullable: !col.is_primary_key, default: None,
408 is_static: col.is_static,
412 })
413 .collect();
414
415 if partition_keys.is_empty() {
416 return Err(Error::schema(
417 "No partition keys found in SSTable header".to_string(),
418 ));
419 }
420
421 let schema = TableSchema {
422 keyspace: header.keyspace.clone(),
423 table: header.table_name.clone(),
424 partition_keys,
425 clustering_keys,
426 columns,
427 comments: HashMap::new(),
428 dropped_columns: HashMap::new(),
429 };
430
431 schema.validate()?;
432 Ok(schema)
433 }
434
435 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
437 let content = fs::read_to_string(path)
438 .map_err(|e| Error::schema(format!("Failed to read schema file: {}", e)))?;
439
440 Self::from_json(&content)
441 }
442
443 pub fn from_json(json: &str) -> Result<Self> {
445 let schema: TableSchema = serde_json::from_str(json)
446 .map_err(|e| Error::schema(format!("Invalid JSON schema: {}", e)))?;
447
448 schema.validate()?;
449 Ok(schema)
450 }
451
452 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
454 let json = serde_json::to_string_pretty(self)
455 .map_err(|e| Error::serialization(format!("Failed to serialize schema: {}", e)))?;
456
457 fs::write(path, json)
458 .map_err(|e| Error::schema(format!("Failed to write schema file: {}", e)))?;
459
460 Ok(())
461 }
462
463 pub fn validate(&self) -> Result<()> {
465 if self.keyspace.is_empty() {
467 return Err(Error::schema("Keyspace name cannot be empty".to_string()));
468 }
469
470 if self.table.is_empty() {
471 return Err(Error::schema("Table name cannot be empty".to_string()));
472 }
473
474 if self.partition_keys.is_empty() {
476 return Err(Error::schema(
477 "Table must have at least one partition key".to_string(),
478 ));
479 }
480
481 let mut positions: Vec<_> = self.partition_keys.iter().map(|k| k.position).collect();
483 positions.sort();
484 for (i, &pos) in positions.iter().enumerate() {
485 if pos != i {
486 return Err(Error::schema(format!(
487 "Partition key positions must be contiguous starting from 0, found gap at position {}",
488 i
489 )));
490 }
491 }
492
493 if !self.clustering_keys.is_empty() {
495 let mut positions: Vec<_> = self.clustering_keys.iter().map(|k| k.position).collect();
496 positions.sort();
497 for (i, &pos) in positions.iter().enumerate() {
498 if pos != i {
499 return Err(Error::schema(format!(
500 "Clustering key positions must be contiguous starting from 0, found gap at position {}",
501 i
502 )));
503 }
504 }
505 }
506
507 for column in &self.columns {
509 CqlType::parse(&column.data_type).map_err(|e| {
510 Error::schema(format!(
511 "Invalid data type '{}' for column '{}': {}",
512 column.data_type, column.name, e
513 ))
514 })?;
515 }
516
517 for key in &self.partition_keys {
523 if !self.columns.iter().any(|c| c.name == key.name) {
524 return Err(Error::schema(format!(
525 "Partition key '{}' not found in columns list",
526 key.name
527 )));
528 }
529 }
530
531 for key in &self.clustering_keys {
532 if !self.columns.iter().any(|c| c.name == key.name) {
533 return Err(Error::schema(format!(
534 "Clustering key '{}' not found in columns list",
535 key.name
536 )));
537 }
538 }
539
540 self.validate_dropped_columns()?;
541
542 Ok(())
543 }
544
545 pub fn for_compaction_output(
569 &self,
570 retained: &std::collections::HashSet<String>,
571 ) -> TableSchema {
572 TableSchema {
573 keyspace: self.keyspace.clone(),
574 table: self.table.clone(),
575 partition_keys: self.partition_keys.clone(),
576 clustering_keys: self.clustering_keys.clone(),
577 columns: self
578 .columns
579 .iter()
580 .filter(|c| {
581 !self.dropped_columns.contains_key(&c.name) || retained.contains(&c.name)
584 })
585 .cloned()
586 .collect(),
587 comments: self.comments.clone(),
588 dropped_columns: HashMap::new(),
589 }
590 }
591
592 pub fn validate_dropped_columns(&self) -> Result<()> {
608 for name in self.dropped_columns.keys() {
609 if !self.columns.iter().any(|c| &c.name == name) {
610 return Err(Error::schema(format!(
611 "dropped column '{}' must remain declared in `columns` (with its type) so \
612 its cells can be decoded and purged during compaction; a dropped column \
613 present only in `dropped_columns` cannot be decoded (see #904/#847)",
614 name
615 )));
616 }
617 }
618 Ok(())
619 }
620
621 pub fn validate_udt_references(&self, registry: &UdtRegistry) -> Result<()> {
633 for column in &self.columns {
634 if let Ok(cql_type) = CqlType::parse(&column.data_type) {
637 self.check_type_udt_references(&cql_type, &column.name, registry)?;
638 }
639 }
640 Ok(())
641 }
642
643 fn check_type_udt_references(
646 &self,
647 cql_type: &CqlType,
648 column_name: &str,
649 registry: &UdtRegistry,
650 ) -> Result<()> {
651 match cql_type {
652 CqlType::Udt(name, _) => {
653 self.ensure_udt_exists(name, column_name, registry)?;
654 }
655 CqlType::Custom(name) => {
664 let udt_name = name.strip_prefix("udt:").unwrap_or(name);
665 if is_udt_identifier(udt_name) {
666 self.ensure_udt_exists(udt_name, column_name, registry)?;
667 }
668 }
669 CqlType::List(inner) | CqlType::Set(inner) | CqlType::Frozen(inner) => {
670 self.check_type_udt_references(inner, column_name, registry)?;
671 }
672 CqlType::Map(key_type, value_type) => {
673 self.check_type_udt_references(key_type, column_name, registry)?;
674 self.check_type_udt_references(value_type, column_name, registry)?;
675 }
676 CqlType::Tuple(field_types) => {
677 for field_type in field_types {
678 self.check_type_udt_references(field_type, column_name, registry)?;
679 }
680 }
681 _ => {} }
683 Ok(())
684 }
685
686 fn ensure_udt_exists(
689 &self,
690 udt_name: &str,
691 column_name: &str,
692 registry: &UdtRegistry,
693 ) -> Result<()> {
694 let (lookup_keyspace, bare_name) = match udt_name.split_once('.') {
697 Some((ks, name)) => (ks, name),
698 None => (self.keyspace.as_str(), udt_name),
699 };
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 pub fn ordered_partition_keys(&self) -> Vec<&KeyColumn> {
730 let mut keys = self.partition_keys.iter().collect::<Vec<_>>();
731 keys.sort_by_key(|k| k.position);
732 keys
733 }
734
735 pub fn ordered_clustering_keys(&self) -> Vec<&ClusteringColumn> {
737 let mut keys = self.clustering_keys.iter().collect::<Vec<_>>();
738 keys.sort_by_key(|k| k.position);
739 keys
740 }
741
742 #[cfg(test)]
744 pub fn new_for_testing(keyspace: &str, table: &str) -> Self {
745 Self {
746 keyspace: keyspace.to_string(),
747 table: table.to_string(),
748 partition_keys: vec![KeyColumn {
749 name: "id".to_string(),
750 data_type: "int".to_string(),
751 position: 0,
752 }],
753 clustering_keys: vec![],
754 columns: vec![Column {
755 name: "id".to_string(),
756 data_type: "int".to_string(),
757 nullable: false,
758 default: None,
759 is_static: false,
760 }],
761 comments: HashMap::new(),
762 dropped_columns: HashMap::new(),
763 }
764 }
765}
766
767#[derive(Debug)]
777pub struct SchemaManager {
778 #[allow(dead_code)]
779 storage: Arc<StorageEngine>,
780 registry: Arc<RwLock<registry::SchemaRegistry>>,
782}
783
784impl SchemaManager {
785 async fn default_registry(
787 platform: Arc<crate::platform::Platform>,
788 config: &Config,
789 ) -> Result<Arc<RwLock<registry::SchemaRegistry>>> {
790 let registry = registry::SchemaRegistry::new(
791 registry::SchemaRegistryConfig::default(),
792 platform,
793 config.clone(),
794 )
795 .await?;
796 Ok(Arc::new(RwLock::new(registry)))
797 }
798
799 pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
801 let config = Config::default();
803 let platform = Arc::new(crate::platform::Platform::new(&config).await?);
804 let storage = Arc::new(
805 StorageEngine::open(
806 path.as_ref(),
807 &config,
808 platform.clone(),
809 #[cfg(feature = "state_machine")]
810 None,
811 )
812 .await?,
813 );
814
815 let registry = Self::default_registry(platform, &config).await?;
816 Ok(Self { storage, registry })
817 }
818
819 pub async fn new_with_storage(storage: Arc<StorageEngine>, config: &Config) -> Result<Self> {
821 let platform = Arc::new(crate::platform::Platform::new(config).await?);
822 let registry = Self::default_registry(platform, config).await?;
823 let manager = Self { storage, registry };
824
825 manager.load_default_udts().await;
827
828 Ok(manager)
829 }
830
831 pub async fn new_with_registry(
846 storage: Arc<StorageEngine>,
847 registry: Arc<tokio::sync::RwLock<registry::SchemaRegistry>>,
848 _config: &Config,
849 ) -> Result<Self> {
850 Ok(Self { storage, registry })
851 }
852
853 #[cfg(test)]
858 pub(crate) fn registry(&self) -> Arc<RwLock<registry::SchemaRegistry>> {
859 self.registry.clone()
860 }
861
862 async fn load_default_udts(&self) {
864 let address_udt = UdtTypeDef::new("test_keyspace".to_string(), "address".to_string())
866 .with_field("street".to_string(), CqlType::Text, true)
867 .with_field("city".to_string(), CqlType::Text, true)
868 .with_field("state".to_string(), CqlType::Text, true)
869 .with_field("zip_code".to_string(), CqlType::Text, true)
870 .with_field("country".to_string(), CqlType::Text, true);
871
872 self.register_udt(address_udt).await;
873
874 let person_udt = UdtTypeDef::new("test_keyspace".to_string(), "person".to_string())
876 .with_field("name".to_string(), CqlType::Text, true)
877 .with_field("age".to_string(), CqlType::Int, true)
878 .with_field("email".to_string(), CqlType::Text, true)
879 .with_field(
880 "addresses".to_string(),
881 CqlType::List(Box::new(CqlType::Udt(
882 "address".to_string(),
883 vec![
884 ("street".to_string(), CqlType::Text),
885 ("city".to_string(), CqlType::Text),
886 ("state".to_string(), CqlType::Text),
887 ("zip_code".to_string(), CqlType::Text),
888 ("country".to_string(), CqlType::Text),
889 ],
890 ))),
891 true,
892 )
893 .with_field(
894 "contact_info".to_string(),
895 CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::Text)),
896 true,
897 );
898
899 self.register_udt(person_udt).await;
900
901 let company_udt = UdtTypeDef::new("test_keyspace".to_string(), "company".to_string())
903 .with_field("name".to_string(), CqlType::Text, false)
904 .with_field(
905 "headquarters".to_string(),
906 CqlType::Udt(
907 "address".to_string(),
908 vec![
909 ("street".to_string(), CqlType::Text),
910 ("city".to_string(), CqlType::Text),
911 ("state".to_string(), CqlType::Text),
912 ("zip_code".to_string(), CqlType::Text),
913 ("country".to_string(), CqlType::Text),
914 ],
915 ),
916 true,
917 )
918 .with_field(
919 "employees".to_string(),
920 CqlType::Set(Box::new(CqlType::Udt("person".to_string(), vec![]))),
921 true,
922 )
923 .with_field("founded_year".to_string(), CqlType::Int, true);
924
925 self.register_udt(company_udt).await;
926 }
927
928 pub async fn register_udt(&self, udt_def: UdtTypeDef) {
930 let _ = self.registry.read().await.register_udt(udt_def).await;
933 }
934
935 pub async fn get_udt(&self, keyspace: &str, name: &str) -> Option<UdtTypeDef> {
937 self.registry
938 .read()
939 .await
940 .get_udt(keyspace, name)
941 .await
942 .ok()
943 .flatten()
944 }
945
946 pub async fn load_schema(&self, table_name: &str) -> Result<TableSchema> {
958 let (keyspace, table) = match table_name.split_once('.') {
961 Some((ks, tbl)) => (Some(ks.to_string()), tbl),
962 None => (None, table_name),
963 };
964 self.find_schema_by_table(&keyspace, table)
965 .await?
966 .ok_or_else(|| {
967 Error::schema(format!(
968 "unknown table {}; no schema registered or discovered",
969 table_name
970 ))
971 })
972 }
973
974 pub async fn parse_and_register_cql_schema(&self, cql: &str) -> Result<TableSchema> {
980 let schema = cql_parser::parse_cql_schema(cql)?;
981 self.registry
982 .read()
983 .await
984 .register_schema(schema.clone(), registry::SchemaSource::Cql(cql.to_string()))
985 .await?;
986 Ok(schema)
987 }
988
989 pub async fn find_schema_by_table(
997 &self,
998 keyspace: &Option<String>,
999 table: &str,
1000 ) -> Result<Option<TableSchema>> {
1001 let found = self
1002 .registry
1003 .read()
1004 .await
1005 .find_schema_by_table(keyspace, table)
1006 .await?;
1007 #[cfg(test)]
1010 if found.is_some() {
1011 TABLE_SCHEMA_CLONES.with(|c| c.set(c.get() + 1));
1012 }
1013 Ok(found)
1014 }
1015
1016 pub fn extract_table_info(&self, cql: &str) -> Result<(Option<String>, String)> {
1018 cql_parser::extract_table_name(cql)
1019 }
1020
1021 pub fn cql_type_to_internal(&self, cql_type: &str) -> Result<CqlTypeId> {
1023 cql_parser::cql_type_to_type_id(cql_type)
1024 }
1025
1026 pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema> {
1028 if let Some(schema) = self.find_schema_by_table(&None, table_name).await? {
1030 Ok(schema)
1031 } else {
1032 Err(Error::Schema(format!(
1033 "Table schema not found: {}",
1034 table_name
1035 )))
1036 }
1037 }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use super::*;
1043
1044 #[test]
1045 fn test_schema_validation() {
1046 let schema_json = r#"
1047 {
1048 "keyspace": "test",
1049 "table": "users",
1050 "partition_keys": [
1051 {"name": "id", "type": "bigint", "position": 0}
1052 ],
1053 "clustering_keys": [],
1054 "columns": [
1055 {"name": "id", "type": "bigint", "nullable": false},
1056 {"name": "name", "type": "text", "nullable": true}
1057 ]
1058 }
1059 "#;
1060
1061 let schema = TableSchema::from_json(schema_json).unwrap();
1062 assert_eq!(schema.keyspace, "test");
1063 assert_eq!(schema.table, "users");
1064 assert_eq!(schema.partition_keys.len(), 1);
1065 assert_eq!(schema.columns.len(), 2);
1066 }
1067
1068 #[test]
1069 fn test_schema_validation_failures() {
1070 let invalid_schema = r#"
1072 {
1073 "keyspace": "test",
1074 "table": "users",
1075 "partition_keys": [],
1076 "clustering_keys": [],
1077 "columns": []
1078 }
1079 "#;
1080
1081 assert!(TableSchema::from_json(invalid_schema).is_err());
1082
1083 let invalid_type = r#"
1085 {
1086 "keyspace": "test",
1087 "table": "users",
1088 "partition_keys": [
1089 {"name": "id", "type": "invalid_type", "position": 0}
1090 ],
1091 "clustering_keys": [],
1092 "columns": [
1093 {"name": "id", "type": "invalid_type", "nullable": false}
1094 ]
1095 }
1096 "#;
1097
1098 assert!(TableSchema::from_json(invalid_type).is_ok());
1100 }
1101
1102 fn udt_schema(column_type: &str) -> TableSchema {
1103 TableSchema {
1104 keyspace: "test_ks".to_string(),
1105 table: "t".to_string(),
1106 partition_keys: vec![KeyColumn {
1107 name: "id".to_string(),
1108 data_type: "uuid".to_string(),
1109 position: 0,
1110 }],
1111 clustering_keys: vec![],
1112 columns: vec![
1113 Column {
1114 name: "id".to_string(),
1115 data_type: "uuid".to_string(),
1116 nullable: false,
1117 default: None,
1118 is_static: false,
1119 },
1120 Column {
1121 name: "value".to_string(),
1122 data_type: column_type.to_string(),
1123 nullable: true,
1124 default: None,
1125 is_static: false,
1126 },
1127 ],
1128 comments: HashMap::new(),
1129 dropped_columns: HashMap::new(),
1130 }
1131 }
1132
1133 #[test]
1134 fn test_udt_reference_undefined_top_level_errors() {
1135 let registry = UdtRegistry::new();
1136 let schema = udt_schema("MyMissingType");
1137
1138 let err = schema
1139 .validate_udt_references(®istry)
1140 .expect_err("undefined UDT reference must fail validation");
1141 let msg = err.to_string();
1142 assert!(
1143 matches!(err, Error::Schema(_)),
1144 "expected schema-category error, got {err:?}"
1145 );
1146 assert!(
1147 msg.contains("MyMissingType"),
1148 "error must name the missing UDT, got: {msg}"
1149 );
1150 }
1151
1152 #[test]
1153 fn test_udt_reference_undefined_lowercase_errors() {
1154 let registry = UdtRegistry::new();
1159 for col_type in ["address", "list<frozen<address>>"] {
1160 let schema = udt_schema(col_type);
1161 let err = schema
1162 .validate_udt_references(®istry)
1163 .expect_err("undefined lowercase UDT must fail validation");
1164 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1165 assert!(
1166 err.to_string().contains("address"),
1167 "error must name the missing UDT, got: {err}"
1168 );
1169 }
1170 }
1171
1172 #[test]
1173 fn test_uppercase_collection_of_primitives_is_not_a_udt() {
1174 let registry = UdtRegistry::new();
1177 for col_type in [
1178 "SET<TEXT>",
1179 "LIST<INT>",
1180 "MAP<TEXT, TEXT>",
1181 "FROZEN<LIST<INT>>",
1182 ] {
1183 let schema = udt_schema(col_type);
1184 schema
1185 .validate_udt_references(®istry)
1186 .unwrap_or_else(|e| panic!("'{col_type}' must not be flagged as a UDT: {e}"));
1187 }
1188 }
1189
1190 #[test]
1191 fn test_uppercase_collection_with_undefined_udt_errors() {
1192 let registry = UdtRegistry::new();
1196 for col_type in [
1197 "LIST<MissingType>",
1198 "MAP<TEXT, MissingType>",
1199 "FROZEN<SET<MissingType>>",
1200 ] {
1201 let schema = udt_schema(col_type);
1202 let err = schema
1203 .validate_udt_references(®istry)
1204 .expect_err("undefined UDT in uppercase collection must fail");
1205 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1206 assert!(
1207 err.to_string().contains("MissingType"),
1208 "error must name the missing UDT, got: {err}"
1209 );
1210 }
1211 }
1212
1213 #[test]
1214 fn test_udt_reference_undefined_nested_in_collection_errors() {
1215 let registry = UdtRegistry::new();
1216 let schema = udt_schema("list<frozen<NestedMissing>>");
1217
1218 let err = schema
1219 .validate_udt_references(®istry)
1220 .expect_err("nested undefined UDT reference must fail validation");
1221 let msg = err.to_string();
1222 assert!(matches!(err, Error::Schema(_)));
1223 assert!(
1224 msg.contains("NestedMissing"),
1225 "error must name the nested missing UDT, got: {msg}"
1226 );
1227 }
1228
1229 #[test]
1230 fn test_udt_reference_undefined_nested_in_map_errors() {
1231 let registry = UdtRegistry::new();
1232 let schema = udt_schema("map<text, MapValueMissing>");
1233
1234 let err = schema
1235 .validate_udt_references(®istry)
1236 .expect_err("undefined UDT in map value must fail validation");
1237 assert!(matches!(err, Error::Schema(_)));
1238 assert!(err.to_string().contains("MapValueMissing"));
1239 }
1240
1241 #[test]
1242 fn test_udt_reference_defined_top_level_ok() {
1243 let mut registry = UdtRegistry::new();
1244 registry.register_udt(
1245 UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1246 "a".to_string(),
1247 CqlType::Text,
1248 true,
1249 ),
1250 );
1251 let schema = udt_schema("MyType");
1252 schema
1253 .validate_udt_references(®istry)
1254 .expect("defined UDT should validate");
1255 }
1256
1257 #[test]
1258 fn test_udt_reference_defined_nested_ok() {
1259 let mut registry = UdtRegistry::new();
1260 registry.register_udt(
1261 UdtTypeDef::new("test_ks".to_string(), "MyType".to_string()).with_field(
1262 "a".to_string(),
1263 CqlType::Text,
1264 true,
1265 ),
1266 );
1267 let schema = udt_schema("list<frozen<MyType>>");
1268 schema
1269 .validate_udt_references(®istry)
1270 .expect("defined nested UDT should validate");
1271 }
1272
1273 #[test]
1274 fn test_validate_udt_references_no_udts_ok() {
1275 let registry = UdtRegistry::new();
1277 let schema = udt_schema("map<text, list<int>>");
1278 schema
1279 .validate_udt_references(®istry)
1280 .expect("primitive/collection-only schema should validate");
1281 }
1282
1283 async fn build_storage() -> Arc<StorageEngine> {
1284 let config = Config::default();
1285 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1286 let temp_dir = tempfile::tempdir().unwrap();
1287 Arc::new(
1288 StorageEngine::open(
1289 temp_dir.path(),
1290 &config,
1291 platform,
1292 #[cfg(feature = "state_machine")]
1293 None,
1294 )
1295 .await
1296 .unwrap(),
1297 )
1298 }
1299
1300 async fn build_shared_registry() -> Arc<RwLock<registry::SchemaRegistry>> {
1301 let config = Config::default();
1302 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1303 Arc::new(RwLock::new(
1304 registry::SchemaRegistry::new(
1305 registry::SchemaRegistryConfig::default(),
1306 platform,
1307 config,
1308 )
1309 .await
1310 .unwrap(),
1311 ))
1312 }
1313
1314 async fn test_manager() -> Arc<SchemaManager> {
1315 let config = Config::default();
1316 let storage = build_storage().await;
1317 Arc::new(
1318 SchemaManager::new_with_storage(storage, &config)
1319 .await
1320 .unwrap(),
1321 )
1322 }
1323
1324 fn table_schema_named(table_name: &str) -> TableSchema {
1325 let mut schema = udt_schema("text");
1326 schema.table = table_name.to_string();
1327 schema
1328 }
1329
1330 #[tokio::test]
1334 async fn test_load_schema_unknown_table_errs() {
1335 let manager = test_manager().await;
1336
1337 let err = manager
1338 .load_schema("never_defined_table")
1339 .await
1340 .expect_err("unknown table must error, not fabricate a schema");
1341 assert!(matches!(err, Error::Schema(_)), "got {err:?}");
1342 assert!(
1343 err.to_string().contains("never_defined_table"),
1344 "error must name the unknown table, got: {err}"
1345 );
1346
1347 let stats = manager
1349 .registry()
1350 .read()
1351 .await
1352 .get_statistics()
1353 .await
1354 .unwrap();
1355 assert_eq!(
1356 stats.total_schemas, 0,
1357 "unknown-table lookup must not mutate the registry"
1358 );
1359 }
1360
1361 #[tokio::test]
1364 async fn test_concurrent_schema_access() {
1365 let manager = test_manager().await;
1366
1367 for i in 0..3 {
1370 let name = format!("table_{}", i);
1371 manager
1372 .registry()
1373 .read()
1374 .await
1375 .register_schema(table_schema_named(&name), registry::SchemaSource::Manual)
1376 .await
1377 .unwrap();
1378 }
1379
1380 let mut handles = vec![];
1382 for i in 0..10 {
1383 let m = Arc::clone(&manager);
1384 let handle = tokio::spawn(async move {
1385 let table = format!("table_{}", i % 3);
1386 let schema = m.load_schema(&table).await.expect("registered table loads");
1387 assert_eq!(schema.table, table);
1388 });
1389 handles.push(handle);
1390 }
1391 for handle in handles {
1392 handle.await.unwrap();
1393 }
1394
1395 let stats = manager
1397 .registry()
1398 .read()
1399 .await
1400 .get_statistics()
1401 .await
1402 .unwrap();
1403 assert_eq!(stats.total_schemas, 3);
1404 for i in 0..3 {
1405 assert!(manager.load_schema(&format!("table_{}", i)).await.is_ok());
1406 }
1407 }
1408
1409 #[tokio::test]
1415 async fn test_manager_reflects_registry_updates_no_stale_snapshot() {
1416 let registry = build_shared_registry().await;
1417
1418 let v1 = table_schema_named("evolving");
1420 assert_eq!(v1.columns.len(), 2);
1421 registry
1422 .read()
1423 .await
1424 .register_schema(v1.clone(), registry::SchemaSource::Manual)
1425 .await
1426 .unwrap();
1427
1428 let storage = build_storage().await;
1429 let config = Config::default();
1430 let manager = SchemaManager::new_with_registry(storage, registry.clone(), &config)
1431 .await
1432 .unwrap();
1433
1434 let got_v1 = manager.get_table_schema("evolving").await.unwrap();
1435 assert_eq!(got_v1.columns.len(), 2, "manager must see v1");
1436
1437 let mut v2 = v1.clone();
1439 v2.columns.push(Column {
1440 name: "extra".to_string(),
1441 data_type: "int".to_string(),
1442 nullable: true,
1443 default: None,
1444 is_static: false,
1445 });
1446 registry
1447 .read()
1448 .await
1449 .register_schema(v2, registry::SchemaSource::Manual)
1450 .await
1451 .unwrap();
1452
1453 let got_v2 = manager.get_table_schema("evolving").await.unwrap();
1454 assert_eq!(
1455 got_v2.columns.len(),
1456 3,
1457 "manager must resolve THROUGH the registry, not a stale by-value snapshot"
1458 );
1459 }
1460
1461 #[tokio::test]
1469 async fn test_manager_does_not_serve_ttl_expired_schema() {
1470 let config = Config::default();
1471 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1472 let mut reg_config = registry::SchemaRegistryConfig::default();
1473 reg_config.cache_ttl_seconds = 0; let registry = Arc::new(RwLock::new(
1475 registry::SchemaRegistry::new(reg_config, platform, config.clone())
1476 .await
1477 .unwrap(),
1478 ));
1479
1480 registry
1482 .read()
1483 .await
1484 .register_schema(
1485 table_schema_named("stale_tbl"),
1486 registry::SchemaSource::Manual,
1487 )
1488 .await
1489 .unwrap();
1490
1491 let storage = build_storage().await;
1492 let manager = SchemaManager::new_with_registry(storage, registry.clone(), &config)
1493 .await
1494 .unwrap();
1495
1496 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1498
1499 let loaded = manager.load_schema("stale_tbl").await;
1503 assert!(
1504 loaded.is_err(),
1505 "manager must delegate TTL freshness to the registry, not serve a stale schema; got {loaded:?}"
1506 );
1507 let got = manager.get_table_schema("stale_tbl").await;
1508 assert!(
1509 got.is_err(),
1510 "get_table_schema must also honor registry expiry; got {got:?}"
1511 );
1512
1513 assert!(
1515 manager.load_schema("never_defined_here").await.is_err(),
1516 "unknown table still errors (no fabrication)"
1517 );
1518 }
1519
1520 #[tokio::test]
1526 async fn test_all_constructors_share_udt_registry() {
1527 let temp = tempfile::tempdir().unwrap();
1529 let via_new = SchemaManager::new(temp.path()).await.unwrap();
1530
1531 let config = Config::default();
1533 let via_storage = {
1534 let storage = build_storage().await;
1535 SchemaManager::new_with_storage(storage, &config)
1536 .await
1537 .unwrap()
1538 };
1539
1540 let via_registry = {
1542 let registry = build_shared_registry().await;
1543 let storage = build_storage().await;
1544 SchemaManager::new_with_registry(storage, registry, &config)
1545 .await
1546 .unwrap()
1547 };
1548
1549 for (label, manager) in [
1550 ("new", &via_new),
1551 ("new_with_storage", &via_storage),
1552 ("new_with_registry", &via_registry),
1553 ] {
1554 let point = UdtTypeDef::new("ks_share".to_string(), "point".to_string()).with_field(
1556 "x".to_string(),
1557 CqlType::Int,
1558 true,
1559 );
1560 manager
1561 .registry()
1562 .read()
1563 .await
1564 .register_udt(point)
1565 .await
1566 .unwrap();
1567 assert!(
1568 manager.get_udt("ks_share", "point").await.is_some(),
1569 "[{label}] UDT registered in the shared registry must be visible via the manager"
1570 );
1571
1572 let line = UdtTypeDef::new("ks_share".to_string(), "line".to_string()).with_field(
1574 "len".to_string(),
1575 CqlType::Int,
1576 true,
1577 );
1578 manager.register_udt(line).await;
1579 let seen = manager
1580 .registry()
1581 .read()
1582 .await
1583 .get_udt("ks_share", "line")
1584 .await
1585 .unwrap();
1586 assert!(
1587 seen.is_some(),
1588 "[{label}] UDT registered via the manager must be visible in the shared registry"
1589 );
1590 }
1591 }
1592
1593 #[test]
1594 fn test_schema_from_sstable_header() {
1595 use crate::parser::header::{
1596 CassandraVersion, ColumnInfo, CompressionInfo, SSTableHeader, SSTableStats,
1597 };
1598 use std::collections::HashMap;
1599
1600 let columns = vec![
1601 ColumnInfo {
1602 name: "id".to_string(),
1603 column_type: "int".to_string(),
1604 is_primary_key: true,
1605 key_position: Some(0),
1606 is_static: false,
1607 is_clustering: false,
1608 clustering_reversed: false,
1609 },
1610 ColumnInfo {
1611 name: "name".to_string(),
1612 column_type: "text".to_string(),
1613 is_primary_key: false,
1614 key_position: None,
1615 is_static: false,
1616 is_clustering: false,
1617 clustering_reversed: false,
1618 },
1619 ];
1620
1621 let header = SSTableHeader {
1622 cassandra_version: CassandraVersion::V5_0Bti,
1623 version: 1,
1624 table_id: [0; 16],
1625 keyspace: "test_ks".to_string(),
1626 table_name: "test_table".to_string(),
1627 generation: 1,
1628 compression: CompressionInfo {
1629 algorithm: "NONE".to_string(),
1630 chunk_size: 0,
1631 parameters: HashMap::new(),
1632 },
1633 stats: SSTableStats::default(),
1634 columns,
1635 properties: HashMap::new(),
1636 };
1637
1638 let schema = TableSchema::from_sstable_header(&header).unwrap();
1639
1640 assert_eq!(schema.keyspace, "test_ks");
1641 assert_eq!(schema.table, "test_table");
1642 assert_eq!(schema.partition_keys.len(), 1);
1643 assert_eq!(schema.partition_keys[0].name, "id");
1644 assert_eq!(schema.columns.len(), 2);
1645 }
1646}