use std::path::Path;
fn is_table_id_dir(name: &str) -> bool {
match name.rsplit_once('-') {
Some((table_name, id)) => {
!table_name.is_empty()
&& id.len() == 32
&& id
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, 'a'..='f'))
}
None => false,
}
}
pub(crate) fn resolve_table_dir(path: &Path) -> Option<&Path> {
let parent = path.parent()?;
if let Some(maybe_snapshots) = parent.parent() {
let is_snapshots_dir =
maybe_snapshots.file_name().and_then(|n| n.to_str()) == Some("snapshots");
if is_snapshots_dir {
if let Some(real_table_dir) = maybe_snapshots.parent() {
let looks_like_table = real_table_dir
.file_name()
.and_then(|n| n.to_str())
.map(is_table_id_dir)
.unwrap_or(false);
if looks_like_table {
return Some(real_table_dir);
}
}
}
}
Some(parent)
}
pub(crate) fn extract_keyspace(path: &Path) -> Option<String> {
resolve_table_dir(path)
.and_then(|table_dir| table_dir.parent())
.and_then(|keyspace_dir| keyspace_dir.file_name())
.and_then(|n| n.to_str())
.map(|s| s.to_string())
}
pub(crate) fn extract_table_name(path: &Path) -> Option<String> {
let dir_name = resolve_table_dir(path)?.file_name()?.to_str()?;
match dir_name.rsplit_once('-') {
Some((table_name, id)) if is_table_id_suffix(id) => Some(table_name.to_string()),
_ => Some(dir_name.to_string()),
}
}
fn is_table_id_suffix(id: &str) -> bool {
id.len() == 32
&& id
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, 'a'..='f'))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_layout_resolves_keyspace_and_table() {
let path = Path::new(
"/var/lib/cassandra/data/myks/mytable-9f3a1c2d4e5f6071829304a5b6c7d8e9/nb-2-big-Data.db",
);
assert_eq!(extract_keyspace(path).as_deref(), Some("myks"));
assert_eq!(extract_table_name(path).as_deref(), Some("mytable"));
}
#[test]
fn snapshot_layout_resolves_real_keyspace_and_table() {
let path = Path::new(
"/var/lib/cassandra/data/myks/mytable-9f3a1c2d4e5f6071829304a5b6c7d8e9/\
snapshots/cqlite-3b1e7f90/nb-2-big-Data.db",
);
assert_eq!(extract_keyspace(path).as_deref(), Some("myks"));
assert_eq!(extract_table_name(path).as_deref(), Some("mytable"));
}
#[test]
fn ordinary_table_in_snapshots_named_keyspace_is_not_a_snapshot() {
let path = Path::new(
"/var/lib/cassandra/data/snapshots/mytable-9f3a1c2d4e5f6071829304a5b6c7d8e9/\
nb-1-big-Data.db",
);
assert_eq!(
extract_keyspace(path).as_deref(),
Some("snapshots"),
"keyspace literally named 'snapshots' must be preserved"
);
assert_eq!(
extract_table_name(path).as_deref(),
Some("mytable"),
"table must resolve normally, not be misread as a snapshot tag"
);
}
#[test]
fn snapshot_tag_with_arbitrary_name_still_resolves() {
let path = Path::new(
"/data/analytics/events-00112233445566778899aabbccddeeff/\
snapshots/pre-upgrade-2026/nb-5-big-Data.db",
);
assert_eq!(extract_keyspace(path).as_deref(), Some("analytics"));
assert_eq!(extract_table_name(path).as_deref(), Some("events"));
}
#[test]
fn table_name_with_internal_hyphen_survives() {
let path = Path::new(
"/mnt/disk3/cassandra/data/ks_ts/sensor-readings-abcdef0123456789abcdef0123456789/\
da-11-bti-Data.db",
);
assert_eq!(extract_keyspace(path).as_deref(), Some("ks_ts"));
assert_eq!(extract_table_name(path).as_deref(), Some("sensor-readings"));
}
#[test]
fn is_table_id_dir_shape() {
assert!(is_table_id_dir("t-9f3a1c2d4e5f6071829304a5b6c7d8e9"));
assert!(is_table_id_dir("my-t-00112233445566778899aabbccddeeff"));
assert!(!is_table_id_dir("t-9f3a"));
assert!(!is_table_id_dir("t-9F3A1C2D4E5F6071829304A5B6C7D8E9"));
assert!(!is_table_id_dir("t-zzzz1c2d4e5f6071829304a5b6c7d8e9"));
assert!(!is_table_id_dir("snapshots"));
assert!(!is_table_id_dir("-9f3a1c2d4e5f6071829304a5b6c7d8e9"));
}
#[test]
fn idless_snapshot_currently_unresolved_pending_followup() {
let path = Path::new("/data/myks/mytable/snapshots/cqlite-abc/nb-2-big-Data.db");
assert_eq!(
extract_keyspace(path).as_deref(),
Some("snapshots"),
"known limitation (#2415): ID-less snapshot keyspace misparses as 'snapshots'"
);
assert_eq!(
extract_table_name(path).as_deref(),
Some("cqlite-abc"),
"known limitation (#2415): ID-less snapshot table misparses as the snapshot tag"
);
}
#[test]
fn too_shallow_paths_return_none() {
let path = Path::new("nb-1-big-Data.db");
assert_eq!(extract_keyspace(path), None);
assert_eq!(extract_table_name(path), None);
}
}