cqlite-core 0.15.0

Core engine for CQLite — read Apache Cassandra 5.0 SSTables locally without a cluster
Documentation
//! SSTable version-letter gates for Cassandra BIG and BTI formats.
//!
//! This module implements the per-letter feature-gate logic that mirrors
//! `BigFormat.java` and `BtiFormat.java` from Cassandra 5.0.8.  Each gate
//! is a `bool` field derived **only** from the two-letter version string found
//! in the SSTable filename prefix (e.g. `nb`, `oa`, `da`).
//!
//! ## Authority chain
//!
//! Cassandra 5.0.8 source (primary) > audit report B10 Part 2 > guide ch.22
//!
//! ### BIG format version letters (BigFormat.java:341-526)
//!
//! | Letter | Cassandra release | Notable additions |
//! |--------|-------------------|--------------------|
//! | `ma`   | 3.0.0             | Native row storage, BF hash swap |
//! | `mb`   | 3.0.7 / 3.7       | Commit-log lower bound |
//! | `mc`   | 3.0.8 / 3.9       | Commit-log intervals |
//! | `md`   | 3.0.18 / 3.11.4   | Accurate min/max clustering |
//! | `me`   | 3.0.25 / 3.11.11  | Originating host ID (first appearance) |
//! | `na`   | 4.0-rc1           | Uncompressed chunks, pending repair, metadata checksum |
//! | `nb`   | 4.0-rc2           | Default BIG letter for stock Cassandra 5.0 compat mode |
//! | `oa`   | 5.0               | Improved min/max, uint deletion time, key range, token coverage |
//!
//! ### BTI format version letters (BtiFormat.java:287-420)
//!
//! | Letter | Cassandra release | Notes |
//! |--------|------------------|-------|
//! | `da`   | 5.0              | Only BTI letter; all gates TRUE |
//!
//! ## Storage-compatibility-mode note
//!
//! Stock Cassandra 5.0 writes **`nb`-versioned BIG** SSTables when
//! `storage_compatibility_mode` is `CASSANDRA_4` (the default).  `oa` is
//! only written after explicitly raising the mode to `NONE`.
//!
//! ## SSTable ID forms (Descriptor.java:85, 95)
//!
//! Cassandra 5.0 supports **two** SSTable ID forms:
//! - Sequential: `nb-1-big-Data.db`  (integer id)
//! - UUID-based: `nb-6aa08200a25111f0a3fef1a551383fb9-big-Data.db` (hex string)
//!
//! Both forms are generated by real Cassandra 5.0 clusters; the UUID form is
//! the default since 5.0.0 (`uuid_sstable_identifiers_enabled: true`).
//!
//! ## Module layout
//!
//! This module is split by concern (epic #1116); the public surface is
//! re-exported here so callers keep importing from
//! `crate::storage::sstable::version_gate::*`:
//! - [`descriptor`] — [`SsTableFormat`] and [`SsTableDescriptor`] filename parsing.
//! - [`big`] — [`BigVersionGates`] (`BigFormat.java`).
//! - [`bti`] — [`BtiVersionGates`] (`BtiFormat.java`).

use std::path::Path;

use crate::Result;

mod big;
mod bti;
mod descriptor;

pub use big::BigVersionGates;
pub use bti::BtiVersionGates;
pub use descriptor::{SsTableDescriptor, SsTableFormat};

/// Combined version-gate result for any SSTable (BIG or BTI).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionGates {
    /// BIG-format gates.
    Big(BigVersionGates),
    /// BTI-format gates.
    Bti(BtiVersionGates),
}

impl VersionGates {
    /// Compute gates from a parsed `SsTableDescriptor`.
    pub fn from_descriptor(desc: &SsTableDescriptor) -> Result<Self> {
        match desc.format {
            SsTableFormat::Big => BigVersionGates::from_version(&desc.version).map(Self::Big),
            SsTableFormat::Bti => BtiVersionGates::from_version(&desc.version).map(Self::Bti),
        }
    }

    /// Compute gates directly from a file path.
    pub fn from_path(path: &Path) -> Result<Self> {
        let desc = SsTableDescriptor::parse(path)?;
        Self::from_descriptor(&desc)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Error;
    use std::path::PathBuf;

    #[test]
    fn test_version_gates_from_path_nb() {
        let path = PathBuf::from("nb-1-big-Data.db");
        let gates = VersionGates::from_path(&path).unwrap();
        match gates {
            VersionGates::Big(g) => assert_eq!(g.version, "nb"),
            VersionGates::Bti(_) => panic!("Expected Big"),
        }
    }

    #[test]
    fn test_version_gates_from_path_oa() {
        let path = PathBuf::from("oa-1-big-Data.db");
        let gates = VersionGates::from_path(&path).unwrap();
        match gates {
            VersionGates::Big(g) => {
                assert_eq!(g.version, "oa");
                assert!(g.has_uint_deletion_time);
            }
            VersionGates::Bti(_) => panic!("Expected Big"),
        }
    }

    /// #1297: an unknown BTI version is rejected through the combined
    /// `VersionGates::from_descriptor` entry point (the public derivation used
    /// by readers), not just the `BtiVersionGates` helper. The BTI allowlist is
    /// exactly `{da}`.
    #[test]
    fn test_version_gates_from_descriptor_rejects_unknown_bti() {
        for v in &["db", "ea", "dz", "zz"] {
            let desc = SsTableDescriptor {
                version: v.to_string(),
                sstable_id: "1".to_string(),
                format: SsTableFormat::Bti,
                component: "Data.db".to_string(),
            };
            match VersionGates::from_descriptor(&desc) {
                Err(Error::UnsupportedVersion { version, floor }) => {
                    assert_eq!(version, *v, "error must name the offending BTI version");
                    assert_eq!(floor, "da", "error must name the da floor");
                }
                other => panic!("{}: expected UnsupportedVersion, got {:?}", v, other),
            }
        }
        // The single supported BTI version still constructs gates via the
        // combined entry point.
        let da_desc = SsTableDescriptor {
            version: "da".to_string(),
            sstable_id: "1".to_string(),
            format: SsTableFormat::Bti,
            component: "Data.db".to_string(),
        };
        assert!(matches!(
            VersionGates::from_descriptor(&da_desc),
            Ok(VersionGates::Bti(_))
        ));
    }

    /// #1297: an unknown above-floor BIG version is rejected through
    /// `VersionGates::from_descriptor` (the reader's derivation path).
    #[test]
    fn test_version_gates_from_descriptor_rejects_unknown_big() {
        let desc = SsTableDescriptor {
            version: "nc".to_string(),
            sstable_id: "1".to_string(),
            format: SsTableFormat::Big,
            component: "Data.db".to_string(),
        };
        assert!(matches!(
            VersionGates::from_descriptor(&desc),
            Err(Error::UnsupportedVersion { .. })
        ));
    }

    #[test]
    fn test_version_gates_from_path_da() {
        let path = PathBuf::from("da-1-bti-Partitions.db");
        let gates = VersionGates::from_path(&path).unwrap();
        match gates {
            VersionGates::Bti(g) => assert_eq!(g.version, "da"),
            VersionGates::Big(_) => panic!("Expected Bti"),
        }
    }

    /// `StatisticsReader::open` derives gates from the Statistics.db component
    /// filename to decode the version-sensitive STATS extras (#1073). Confirm the
    /// descriptor parses a non-Data.db component so modern (oa/da) gates are
    /// available on that read path, not just for Data.db.
    #[test]
    fn test_version_gates_from_statistics_component_path() {
        let nb = VersionGates::from_path(&PathBuf::from("nb-1-big-Statistics.db")).unwrap();
        assert!(matches!(nb, VersionGates::Big(g) if g.version == "nb"));
        let da = VersionGates::from_path(&PathBuf::from("da-1-bti-Statistics.db")).unwrap();
        assert!(matches!(da, VersionGates::Bti(g) if g.version == "da"));
    }

    /// Verify that UUID-based ids (corpus filenames) parse correctly into gates.
    #[test]
    fn test_version_gates_from_corpus_filename() {
        let path = PathBuf::from("nb-6aa08200a25111f0a3fef1a551383fb9-big-Data.db");
        let gates = VersionGates::from_path(&path).unwrap();
        match gates {
            VersionGates::Big(g) => {
                assert_eq!(g.version, "nb");
                // oa-only gates must be absent
                assert!(!g.has_improved_min_max);
                assert!(!g.has_uint_deletion_time);
            }
            VersionGates::Bti(_) => panic!("Expected Big"),
        }
    }

    // -----------------------------------------------------------------------
    // Docker-generated fixture gates
    // These filenames come from Cassandra 5.0.8 containers run with:
    //   storage_compatibility_mode: NONE  (for oa)
    //   sstable.selected_format: bti       (for da)
    // -----------------------------------------------------------------------

    /// Gates for the Docker-generated `oa` fixture must have all 5 oa-only
    /// gates TRUE.
    #[test]
    fn test_gates_docker_oa_fixture() {
        let gates = VersionGates::from_path(&PathBuf::from("oa-2-big-Data.db")).unwrap();
        match gates {
            VersionGates::Big(g) => {
                assert_eq!(g.version, "oa");
                assert!(g.has_improved_min_max, "oa fixture: hasImprovedMinMax");
                assert!(
                    g.has_partition_level_deletion_presence_marker,
                    "oa fixture: hasPartitionLevelDeletionPresenceMarker"
                );
                assert!(g.has_key_range, "oa fixture: hasKeyRange");
                assert!(g.has_uint_deletion_time, "oa fixture: hasUIntDeletionTime");
                assert!(
                    g.has_token_space_coverage,
                    "oa fixture: hasTokenSpaceCoverage"
                );
                // deprecated in oa
                assert!(
                    !g.has_accurate_min_max,
                    "oa fixture: hasAccurateMinMax deprecated"
                );
                assert!(
                    !g.has_legacy_min_max,
                    "oa fixture: hasLegacyMinMax deprecated"
                );
            }
            VersionGates::Bti(_) => panic!("Expected Big gates for oa-2-big-Data.db"),
        }
    }

    /// Gates for the Docker-generated `da` fixture: all BTI gates TRUE.
    #[test]
    fn test_gates_docker_da_fixture() {
        let gates = VersionGates::from_path(&PathBuf::from("da-2-bti-Data.db")).unwrap();
        match gates {
            VersionGates::Bti(g) => {
                assert_eq!(g.version, "da");
                assert!(g.has_improved_min_max, "da: hasImprovedMinMax");
                assert!(g.has_key_range, "da: hasKeyRange");
                assert!(g.has_uint_deletion_time, "da: hasUIntDeletionTime");
                assert!(g.has_token_space_coverage, "da: hasTokenSpaceCoverage");
                assert!(
                    g.has_partition_level_deletion_presence_marker,
                    "da: hasPartitionLevelDeletionPresenceMarker"
                );
                assert!(!g.has_old_bf_format, "da: !hasOldBfFormat");
                assert!(g.has_originating_host_id, "da: hasOriginatingHostId");
                // BtiFormat.java:363-371
                assert!(g.has_accurate_min_max, "da: hasAccurateMinMax");
                assert!(!g.has_legacy_min_max, "da: !hasLegacyMinMax");
            }
            VersionGates::Big(_) => panic!("Expected Bti gates for da-2-bti-Data.db"),
        }
    }
}