#[cfg(feature = "bench-internals")]
mod bench_shim;
mod block_entries;
pub(crate) mod byte_comparable; pub(crate) mod comparator_value_parsing; mod custom_scalar;
mod key_parsing;
#[cfg(test)]
mod schema_fallback_stall_tests;
pub(crate) mod v5_compressed_legacy;
mod value_parsing;
#[cfg(test)]
mod value_parsing_schema_type_tests;
pub(in crate::storage::sstable::reader) use v5_compressed_legacy::V5CompressedLegacyParser;
pub(in crate::storage::sstable::reader) use v5_compressed_legacy::ParseStep;
#[doc(hidden)]
pub use v5_compressed_legacy::V5CompressedLegacyParser as PublicV5CompressedLegacyParser;
use std::collections::HashMap;
use std::path::Path;
use log::{debug, error, warn};
use crate::{
schema::{ClusteringColumn, Column, KeyColumn, TableSchema},
Error, Result, RowCells, RowKey, ScanRow, Value,
};
use super::{super::row_cell_state_machine::ParsedRow, types::SSTableReader};
pub(in crate::storage::sstable::reader) fn extract_keyspace_table_from_path(
path: &Path,
) -> Result<(String, String)> {
let table_dir = path
.parent()
.ok_or_else(|| Error::schema("SSTable path has no parent directory"))?;
let table_dir_name = table_dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| Error::schema("Invalid table directory name"))?;
let table_name = table_dir_name
.rsplit_once('-')
.ok_or_else(|| {
Error::schema(format!(
"Table directory '{}' does not match 'tablename-uuid' format",
table_dir_name
))
})?
.0
.to_string();
let keyspace_dir = table_dir
.parent()
.ok_or_else(|| Error::schema("Table directory has no parent (keyspace) directory"))?;
let keyspace = keyspace_dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| Error::schema("Invalid keyspace directory name"))?
.to_string();
Ok((keyspace, table_name))
}
impl SSTableReader {
pub(in crate::storage::sstable::reader) fn get_table_schema(
&self,
provided_schema: Option<&TableSchema>,
) -> Option<TableSchema> {
if let Some(schema) = provided_schema {
debug!(
"get_table_schema: Using provided schema for {}.{}",
schema.keyspace, schema.table
);
return Some(schema.clone());
}
if let Some(schema) = self.schema.as_deref() {
debug!(
"get_table_schema: Using schema from SSTable header for {}.{}",
self.header.keyspace, self.header.table_name
);
return Some(schema.clone());
}
#[cfg(feature = "state_machine")]
{
if let Some(schema) = self.registry_schema.as_deref() {
debug!(
"get_table_schema: Using pre-resolved registry schema for {}.{}",
schema.keyspace, schema.table
);
return Some(schema.clone());
}
}
#[cfg(not(feature = "state_machine"))]
{
if let Some(registry) = self.schema_registry.as_ref() {
let (keyspace, table_name) = match extract_keyspace_table_from_path(&self.file_path)
{
Ok(names) => names,
Err(e) => {
debug!(
"get_table_schema: Failed to extract names from path {}: {}. Falling back to header names.",
self.file_path.display(), e
);
(self.header.keyspace.clone(), self.header.table_name.clone())
}
};
debug!("get_table_schema: Schema registry lookup not available in non-state_machine builds");
let _ = (registry, &keyspace, &table_name); }
}
debug!(
"get_table_schema: Falling back to header column construction for {}.{}",
self.header.keyspace, self.header.table_name
);
if self.header.columns.is_empty() {
return None;
}
let mut columns = Vec::new();
let mut partition_keys = Vec::new();
let mut clustering_keys = Vec::new();
for col_info in self.header.columns.iter() {
let column = Column {
name: col_info.name.clone(),
data_type: col_info.column_type.clone(), nullable: true,
default: None,
is_static: col_info.is_static,
};
if col_info.is_primary_key && !col_info.is_clustering {
partition_keys.push(KeyColumn {
name: col_info.name.clone(),
data_type: col_info.column_type.clone(),
position: partition_keys.len(),
});
} else if col_info.is_clustering {
clustering_keys.push(ClusteringColumn {
name: col_info.name.clone(),
data_type: col_info.column_type.clone(),
position: clustering_keys.len(),
order: crate::schema::ClusteringOrder::Asc,
});
}
columns.push(column);
}
Some(TableSchema {
keyspace: self.header.keyspace.clone(),
table: self.header.table_name.clone(),
partition_keys,
clustering_keys,
columns,
comments: HashMap::new(),
dropped_columns: HashMap::new(),
})
}
pub(in crate::storage::sstable::reader) fn parse_partition_data(
&self,
data: &[u8],
schema: Option<&crate::schema::TableSchema>,
) -> Result<Option<Vec<(RowKey, ScanRow)>>> {
if data.is_empty() {
return Ok(Some(Vec::new()));
}
let mut state_machine = if let Some(schema) = schema {
match schema.get_partition_key_comparators() {
Ok(comparators) if !comparators.is_empty() => {
debug!("parse_partition_data: Creating schema-aware state machine with {} partition key comparators", comparators.len());
super::super::row_cell_state_machine::RowCellStateMachine::with_schema_and_version(
schema.clone(),
comparators[0].clone(),
self.header.cassandra_version
)
}
Ok(_empty) => {
warn!(
"parse_partition_data: Schema for {}.{} has {} partition keys but comparator parsing returned empty - falling back to schemaless parsing",
schema.keyspace, schema.table, schema.partition_keys.len()
);
super::super::row_cell_state_machine::RowCellStateMachine::new()
}
Err(e) => {
warn!(
"parse_partition_data: Failed to get partition key comparators for {}.{}: {} - falling back to schemaless parsing",
schema.keyspace, schema.table, e
);
super::super::row_cell_state_machine::RowCellStateMachine::new()
}
}
} else {
debug!("parse_partition_data: No schema provided, using basic state machine");
super::super::row_cell_state_machine::RowCellStateMachine::new()
};
let mut results = Vec::new();
match state_machine.parse_partition_data(data) {
Ok(parsed_rows) => {
for parsed_row in parsed_rows {
let row_key = self.extract_row_key_from_parsed_row(&parsed_row)?;
let value = if let Some(s) = schema {
self.extract_value_from_parsed_row_with_schema(&parsed_row, s)?
} else {
self.extract_value_from_parsed_row_fallback(&parsed_row)?
};
results.push((row_key, value));
}
Ok(Some(results))
}
Err(e) => {
let context = if let Some(s) = schema {
format!("table {}.{} with schema", s.keyspace, s.table)
} else {
"unknown table without schema".to_string()
};
error!(
"parse_partition_data: Failed to parse partition data for {} (format {:?}): {}",
context, self.header.cassandra_version, e
);
Err(Error::corruption(format!(
"Partition data parsing failed for {} (format {:?}): {}. \
Ensure schema is provided for Cassandra 5.0+ formats (Issue #35 compliance).",
context, self.header.cassandra_version, e
)))
}
}
}
pub(in crate::storage::sstable::reader) fn extract_row_key_from_parsed_row(
&self,
parsed_row: &ParsedRow,
) -> Result<RowKey> {
if let Some(clustering_key) = &parsed_row.clustering_key {
Ok(RowKey::from(clustering_key.clone()))
} else if !parsed_row.clustering_rows.is_empty() {
let first_clustering_row = &parsed_row.clustering_rows[0];
let clustering_key_str = String::from_utf8_lossy(&first_clustering_row.clustering_key);
Ok(RowKey::from(clustering_key_str.to_string()))
} else {
let partition_key_str = String::from_utf8_lossy(&parsed_row.partition_key.key_bytes);
Ok(RowKey::from(format!("partition_{}", partition_key_str)))
}
}
pub(in crate::storage::sstable::reader) fn extract_value_from_parsed_row_fallback(
&self,
parsed_row: &ParsedRow,
) -> Result<ScanRow> {
use std::sync::Arc;
let mut row_cells: RowCells = Vec::new();
for cell in &parsed_row.cells {
if let Some(ref value) = cell.value {
row_cells.push((Arc::from(cell.column_name.as_str()), value.clone()));
}
}
for clustering_row in &parsed_row.clustering_rows {
for (name, value) in &clustering_row.columns {
row_cells.push((Arc::from(name.as_str()), value.clone()));
}
}
if let Some(ref static_row) = parsed_row.static_row {
for (name, value) in &static_row.columns {
row_cells.push((Arc::from(name.as_str()), value.clone()));
}
}
if row_cells.is_empty() {
return Ok(ScanRow::Marker(Value::Null));
}
row_cells.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
Ok(ScanRow::Row(row_cells))
}
pub(in crate::storage::sstable::reader) fn extract_value_from_parsed_row_with_schema(
&self,
parsed_row: &ParsedRow,
schema: &crate::schema::TableSchema,
) -> Result<ScanRow> {
use std::collections::HashMap;
let mut columns: HashMap<String, Value> = HashMap::new();
debug!(
"extract_value_with_schema: Processing row with {} cells",
parsed_row.cells.len()
);
debug!(
"extract_value_with_schema: Schema has {} partition keys, {} clustering keys, {} columns",
schema.partition_keys.len(),
schema.clustering_keys.len(),
schema.columns.len()
);
for (idx, component) in parsed_row.partition_key.components.iter().enumerate() {
if let Some(pk_col) = schema.partition_keys.get(idx) {
debug!(
"extract_value_with_schema: Processing partition key column: {}",
pk_col.name
);
let typed_value =
self.parse_value_with_schema_type(component, &pk_col.data_type)
.unwrap_or_else(|e| {
warn!(
"extract_value_with_schema: Failed to parse partition key {}: {}, using blob fallback",
pk_col.name, e
);
Value::Blob(component.clone())
});
columns.insert(pk_col.name.clone(), typed_value);
}
}
if let Some(ref clustering_key) = parsed_row.clustering_key {
if !parsed_row.clustering_rows.is_empty() {
let first_clustering_row = &parsed_row.clustering_rows[0];
let ck_bytes = &first_clustering_row.clustering_key;
if schema.clustering_keys.len() == 1 {
let ck_col = &schema.clustering_keys[0];
debug!(
"extract_value_with_schema: Processing clustering key column: {}",
ck_col.name
);
let typed_value =
self.parse_value_with_schema_type(ck_bytes, &ck_col.data_type)
.unwrap_or_else(|e| {
warn!(
"extract_value_with_schema: Failed to parse clustering key {}: {}, using blob fallback",
ck_col.name, e
);
Value::Blob(ck_bytes.clone())
});
columns.insert(ck_col.name.clone(), typed_value);
} else if schema.clustering_keys.len() > 1 {
warn!(
"extract_value_with_schema: Composite clustering keys not yet implemented for {}.{} ({} keys) - using string representation fallback",
schema.keyspace, schema.table, schema.clustering_keys.len()
);
for ck_col in &schema.clustering_keys {
columns.insert(ck_col.name.clone(), Value::Text(clustering_key.clone()));
}
}
} else {
for ck_col in &schema.clustering_keys {
columns.insert(ck_col.name.clone(), Value::Text(clustering_key.clone()));
}
}
}
for cell in &parsed_row.cells {
if let Some(col) = schema.columns.iter().find(|c| c.name == cell.column_name) {
debug!(
"extract_value_with_schema: Processing regular column: {}",
cell.column_name
);
if let Some(ref cell_value) = cell.value {
match cell_value {
Value::Blob(bytes) if !bytes.is_empty() => {
let typed_value =
self.parse_value_with_schema_type(bytes, &col.data_type)
.unwrap_or_else(|e| {
debug!(
"extract_value_with_schema: Failed to parse column {}: {}, keeping blob",
cell.column_name, e
);
Value::Blob(bytes.clone())
});
columns.insert(cell.column_name.clone(), typed_value);
}
_ => {
columns.insert(cell.column_name.clone(), cell_value.clone());
}
}
} else {
columns.insert(cell.column_name.clone(), Value::Null);
}
}
}
for clustering_row in &parsed_row.clustering_rows {
for (col_name, col_value) in &clustering_row.columns {
if let Some(col) = schema.columns.iter().find(|c| c.name == *col_name) {
debug!(
"extract_value_with_schema: Processing clustering row column: {}",
col_name
);
match col_value {
Value::Blob(bytes) if !bytes.is_empty() => {
let typed_value =
self.parse_value_with_schema_type(bytes, &col.data_type)
.unwrap_or_else(|e| {
debug!(
"extract_value_with_schema: Failed to parse clustering row column {}: {}, keeping blob",
col_name, e
);
Value::Blob(bytes.clone())
});
columns.insert(col_name.clone(), typed_value);
}
_ => {
columns.insert(col_name.clone(), col_value.clone());
}
}
}
}
}
if let Some(ref static_row) = parsed_row.static_row {
for (col_name, col_value) in &static_row.columns {
if let Some(col) = schema.columns.iter().find(|c| c.name == *col_name) {
debug!(
"extract_value_with_schema: Processing static row column: {}",
col_name
);
match col_value {
Value::Blob(bytes) if !bytes.is_empty() => {
let typed_value =
self.parse_value_with_schema_type(bytes, &col.data_type)
.unwrap_or_else(|e| {
debug!(
"extract_value_with_schema: Failed to parse static row column {}: {}, keeping blob",
col_name, e
);
Value::Blob(bytes.clone())
});
columns.insert(col_name.clone(), typed_value);
}
_ => {
columns.insert(col_name.clone(), col_value.clone());
}
}
}
}
}
if columns.is_empty() {
return Err(Error::Schema(format!(
"No columns matched schema - parsed {} cells but none matched {} schema columns",
parsed_row.cells.len(),
schema.columns.len()
)));
}
debug!(
"extract_value_with_schema: Extracted {} columns into row map",
columns.len()
);
let mut row_cells: RowCells = columns
.into_iter()
.map(|(name, value)| (std::sync::Arc::from(name.as_str()), value))
.collect();
row_cells.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
Ok(ScanRow::Row(row_cells))
}
pub(in crate::storage::sstable::reader) async fn parse_partition_at_offset(
&self,
offset: u64,
size: u32,
) -> Result<Option<Vec<(RowKey, ScanRow)>>> {
let buffer = self
.read_uncompressed_verified(&self.file, offset, size as usize)
.await?;
let data = if let Some(compression_reader) = &self.compression_reader {
let compression =
super::super::compression::Compression::new(*compression_reader.algorithm())?;
match compression.decompress(&buffer) {
Ok(decompressed) => decompressed,
Err(e) => {
log::warn!("Decompression failed at offset {}: {}", offset, e);
buffer }
}
} else {
buffer
};
let table_schema = self.get_table_schema(None);
self.parse_partition_data(&data, table_schema.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_extract_keyspace_table_standard_format() {
let path = Path::new("/data/sstables/test_basic/simple_table-6b0425d0a25111f0a3fef1a551383fb9/nb-1-big-Data.db");
let (keyspace, table) = extract_keyspace_table_from_path(path).unwrap();
assert_eq!(keyspace, "test_basic");
assert_eq!(table, "simple_table");
}
#[test]
fn test_extract_keyspace_table_with_hyphens() {
let path = Path::new("/data/sstables/my_keyspace/user-profiles-xyz789/nb-1-big-Data.db");
let (keyspace, table) = extract_keyspace_table_from_path(path).unwrap();
assert_eq!(keyspace, "my_keyspace");
assert_eq!(table, "user-profiles");
}
#[test]
fn test_extract_keyspace_table_real_test_data() {
let path = Path::new("/Users/patrick/local_projects/cqlite/test-data/datasets/sstables/test_basic/simple_table-6de93b70934a11f08d448925b7a9e804/nb-1-big-Data.db");
let (keyspace, table) = extract_keyspace_table_from_path(path).unwrap();
assert_eq!(keyspace, "test_basic");
assert_eq!(table, "simple_table");
}
#[test]
fn test_extract_keyspace_table_collections() {
let path = Path::new("test-data/datasets/sstables/test_collections/collection_table-6b8c8fb0a25111f0a3fef1a551383fb9/nb-1-big-Data.db");
let (keyspace, table) = extract_keyspace_table_from_path(path).unwrap();
assert_eq!(keyspace, "test_collections");
assert_eq!(table, "collection_table");
}
#[test]
fn test_extract_keyspace_table_invalid_no_parent() {
let path = Path::new("/Data.db");
let result = extract_keyspace_table_from_path(path);
assert!(result.is_err());
}
#[test]
fn test_extract_keyspace_table_invalid_format() {
let path = Path::new("/data/keyspace/tablename/Data.db");
let result = extract_keyspace_table_from_path(path);
assert!(result.is_err());
}
#[test]
fn test_extract_keyspace_table_relative_path() {
let path = Path::new("test_basic/simple_table-abc123/nb-1-big-Data.db");
let (keyspace, table) = extract_keyspace_table_from_path(path).unwrap();
assert_eq!(keyspace, "test_basic");
assert_eq!(table, "simple_table");
}
}