Skip to main content

SSTableReader

Struct SSTableReader 

Source
pub struct SSTableReader {
    pub generation: u64,
    pub compression_info: Option<Arc<CompressionInfo>>,
    /* private fields */
}
Expand description

SSTable reader for efficient data access

Fields§

§generation: u64

SSTable generation number (for multi-generation merging)

§compression_info: Option<Arc<CompressionInfo>>

CompressionInfo metadata for chunked decompression (if compressed)

Implementations§

Source§

impl SSTableReader

Source

pub fn scan_for_key_call_count() -> u64

Current value of the test-only scan_for_key invocation counter.

Issue #831: tests use this to assert that a BTI get() resolves entirely through the Partitions.db trie and never falls through to the sequential scan. See [SCAN_FOR_KEY_CALLS].

Source§

impl SSTableReader

Source

pub async fn iterate_all_partitions_for_compaction( &self, schema: Option<&TableSchema>, ) -> Result<Vec<CompactionRow>>

Iterate all partitions with per-row timestamps, for use by the compaction merger.

Returns (RowKey, ScanRow, row_timestamp_micros) for every row in the SSTable. Unlike iterate_all_partitions:

  • Row tombstones are returned as Value::Tombstone(RowTombstone) carrying the actual deletion timestamp extracted from the on-disk row header.
  • Cell tombstones within live rows are stored as Value::Tombstone(CellTombstone) inside the Value::Map, also carrying the actual cell-level deletion timestamp.
  • The third tuple element is the decoded row-level write timestamp, so the merger can perform timestamp-accurate last-write-wins comparisons.

Normal user-facing reads use scan / get / iterate_all_partitions, which apply tombstone filtering. Do NOT use this method for user-visible queries.

(Issue #505)

Source

pub async fn distinct_partition_count(&self) -> Result<usize>

Count the distinct PARTITION keys decoded from Data.db (issue #970).

This is a partition-granular count — one per partition, never per row — used by the SSTable verifier’s BTI cross-check (verify.rs). It must NOT be confused with get_all_entries, whose RowKeys carry clustering/column/static suffixes and therefore over-count a multi-row partition as many distinct keys.

Both supported Data.db layouts surface the partition key (without any clustering suffix) via the compaction read path:

  • BIG (nb, V5CompressedLegacy + is_nb_format): [iterate_all_partitions_for_compaction] emits one CompactionRow per row, each carrying the partition key (CompactionRow::key). Distinct keys == partition count.
  • BTI (da): the compaction iterator’s non-stitching fallback would route through iterate_all_partitions, whose keys are row-granular. Instead we stitch the data section and run the compaction parser directly, which emits the same partition-key-only CompactionRow::key.

No schema is required from the caller: the parser resolves it via the reader’s header/registry (get_table_schema).

Source

pub async fn distinct_partition_keys(&self) -> Result<Vec<Vec<u8>>>

Return the set of distinct raw partition keys decoded from Data.db, one entry per partition (NOT per row), in first-seen order.

Each key is the raw serialized partition key as stored on disk (e.g. the 4-byte big-endian value for pk int, 16 bytes for a UUID) — the same form accepted by encode_partition_key_for_bti_trie. This is used by the verifier’s BTI Partitions.db cross-check to compare partition-key IDENTITY (issue #1103), not just partition count.

The stitch/parse strategy mirrors Self::distinct_partition_count: we route through stitch_all_chunks (not the generic iterate_all_partitions_for_compaction) for BOTH BIG and BTI so the incompressible/raw-chunk fallback is honoured (issue #970), and parse with the compaction parser, which emits one CompactionRow per partition with the partition key in key (partition-granular). No schema is required from the caller: the parser resolves it via the reader’s header/registry.

Source

pub async fn distinct_partition_keys_with_positions( &self, ) -> Result<Vec<(u64, Vec<u8>)>>

Return the distinct raw partition keys decoded from Data.db together with the byte offset at which each partition begins in the DECOMPRESSED data section (issue #1103).

Each tuple is (data_position, raw_partition_key), where data_position is exactly the value a BTI Partitions.db leaf encodes as BtiPartitionLocation::DataOffset (and the data_position recovered from a RowsOffset entry via resolve_rows_db_entry). The verifier’s BTI cross-check resolves each leaf PAYLOAD back to its raw partition key through this map so it catches a corruption that keeps the emitted trie prefix but rewrites the payload to point at a DIFFERENT partition (a same-count wrong-IDENTITY corruption the prefix-only compare missed).

The stitch/parse strategy mirrors Self::distinct_partition_keys; only the parser entry point differs (it threads the partition-start offset).

Source

pub async fn partition_verify_scan(&self) -> Result<Vec<(Vec<u8>, Option<i32>)>>

Verifier-facing scan (issue #1282): return, in on-disk order, every distinct partition’s raw key together with its raw partition-level localDeletionTime (when the partition carries a tombstone).

Each element is (raw_partition_key, partition_local_deletion_time) where the LDT is Some(i32) only for a DELETED partition (a live partition carries the DeletionTime.LIVE sentinel and yields None). The i32 is exactly the value parse_partition_header_full decodes: for the legacy signed nb form it is the genuine signed i32 BE; for the unsigned oa/da form it is the wrapping as u32 as i32 representation of the on-disk u32 (far-future values in [2^31, 2^32) therefore appear negative and are LEGITIMATE — the caller must consult SSTableReader::has_uint_deletion_time before interpreting a negative value as corrupt).

The ordering is the on-disk partition order, so the verifier can assert ascending Murmur3 token order (Cassandra stores partitions in token order) without a separate scan.

Source

pub async fn partition_clustering_verify_scan( &self, ) -> Result<Vec<(usize, Vec<Vec<Value>>)>>

Verifier-facing scan (issue #1282, roborev follow-up): return, in on-disk order, every partition’s ordered list of decoded CLUSTERING-key tuples.

Each element is (partition_index, Vec<Vec<Value>>), where the inner Vec<Value> is one clustering row’s values in schema clustering order, and the outer Vec holds those rows in the exact ORDER they appear on disk. The verifier compares consecutive tuples within a partition using the authoritative per-column crate::types::comparator::ComparatorType plus each clustering column’s ASC/DESC order, and flags a non-increasing step as crate::storage::sstable::verify::VerifyErrorClass::OutOfOrderKeyOrRow — the “row” half of that class (Cassandra’s Verifier rejects out-of-order clustering rows too).

Rows carrying no clustering prefix (a static row, a partition-level tombstone carrier, or an unclustered table’s single row) are OMITTED from the per-partition list: they do not participate in clustering order.

Reuses the SAME on-disk-order emit-with-offset parser as Self::partition_verify_scan, so the clustering values come from the authoritative schema-aware decode, not a heuristic. Uses only non-gated CQL comparators (no write-support dependency). When the table has no clustering columns the returned list is empty (nothing to order).

Source

pub async fn stream_all_partitions_for_compaction<F>( &self, schema: Option<&TableSchema>, scan_cancel: &ScanCancel, emit: F, ) -> Result<()>

Streaming compaction read (issue #827): yield (RowKey, ScanRow, ts) entries via emit one partition at a time, so peak memory is bounded by max_partition_size + one_chunk rather than by the total input size.

This is the incremental counterpart of [iterate_all_partitions_for_compaction], which fully materialises the decompressed data section and parses every entry into a Vec before returning. The k-way merge producer (merge::producer_thread) uses this to forward entries into its bounded channel directly, so a source’s decompressed content is never fully resident.

§Sliding-window driver

The V5CompressedLegacy chunk-stitching path keeps a sliding WindowCursor of decompressed bytes. After refilling with each decompressed chunk it drains confirmed partitions via parse_one_partition_with_timestamps, advancing the cursor over the front after every Emitted (the reclaimed prefix is compacted once per refill, not memmoved per partition — issue #1589), and stopping at NeedMore to await the next chunk (a partition can straddle a chunk boundary). At EOF a final drain pass runs with at_final_chunk = true so the trailing (possibly truncated) partition is terminal rather than requesting a refill that will never come.

Returning ControlFlow::Break from emit stops the scan early (consumer dropped). Tombstone / timestamp semantics are byte-identical to the Vec variant (Issue #505/#533).

scan_cancel is an explicit PER-CALL cancellation token (issue #2346), not the reader’s own SSTableReader::scan_cancel field: a cached/shared Arc<SSTableReader> (a future warm-handle registry) may drive two concurrent calls to this method with two INDEPENDENT tokens, so cancellation cannot live as mutable per-reader state (set_scan_cancel requires &mut self, uncallable through a shared Arc). Callers that want the reader’s own field’s semantics (the pre-#2346 default) pass &self.scan_cancel explicitly — see SSTableReader::iterate_all_partitions_cancellable for the analogous non-compaction seam.

Source§

impl SSTableReader

Source

pub async fn stream_all_partitions_for_query<F>( &self, schema: Option<&TableSchema>, scan_cancel: &ScanCancel, token_bound: Option<ScanTokenBound>, emit: F, ) -> Result<()>

Query-serve partition enumeration for the WARM reader-based merge (issue #2412 §C / #2413 Option A) — the analogue of stream_all_partitions_for_compaction that the flight do_get warm path drives (from_readers::drive_query_stream).

Routing MIRRORS stream_all_partitions_for_compaction’s per-mechanism emit shape so the merger reconciles identically, but STREAMS the index (no resident Vec, spec R4) and pushes the token range into the walk (#2413):

  • Chunk-stitching (nb) readers use the full-fidelity CompactionRow decoder (stream_partitions_summary_guided_compaction) — byte-identical to drain_compaction_window’s emit, so cross-generation LWW reconciliation is preserved.
  • Non-stitching (V5_0Uncompressed) readers use the read-shadowing ScanRow decoder (stream_partitions_summary_guided) — byte-identical to stream_all_partitions_cancellable’s emit.

A FellBack (no usable summary/index) routes on to the full-ring stream_all_partitions_for_compaction. Compaction consumers do NOT call this (they use the path-based stream directly, no token range), so they keep full-ring byte-parity walks unchanged (spec R3).

Source§

impl SSTableReader

Source

pub async fn read_single_partition_for_compaction( &self, partition_key: &[u8], schema: Option<&TableSchema>, scan_cancel: &ScanCancel, ) -> Result<SinglePartitionCompaction>

Probe one SSTable for a single partition, returning its compaction rows via an authoritative seek — or a prune / scan-fallback signal (issue #2207).

This is the public core primitive the Flight point-read path composes into its existing k-way merge. Correctness spine (fail-open toward reading):

  1. might_contain_partition reports a definite negative → SinglePartitionCompaction::DefinitelyAbsent (pruned + counted).
  2. No random-access index → SinglePartitionCompaction::IndexUnavailable (the caller scans this SSTable).
  3. Otherwise resolve the partition’s uncompressed offset (BTI trie / BIG Index.db) and its authoritative end bound (successor offset / data length), materialize ONLY the covering chunk window, and parse the one partition with the SAME compaction parser the full scan uses → SinglePartitionCompaction::Rows. A BIG Index.db miss (inconclusive, #1572) or an un-boundable last partition degrades to SinglePartitionCompaction::IndexUnavailable.

partition_key is the raw partition-key bytes (as PartitionKey::to_bytes produces). schema is the authoritative table schema (the parser needs column names).

scan_cancel is an explicit PER-CALL cancellation token (issue #2346), mirroring SSTableReader::stream_all_partitions_for_compaction — not the reader’s own scan_cancel field, so a shared/cached Arc<SSTableReader> can serve two concurrent point-read probes with independent cancellation.

Source§

impl SSTableReader

Source

pub async fn verify_presence_oracle_negative( &self, table_id: &TableId, partition_key: &[u8], ) -> Result<bool>

Authoritatively verify a presence-oracle “definitely absent” verdict for partition_key in THIS SSTable (issue #2163, opt-in / default-off).

When the presence_verification switch is OFF (the default) this returns Ok(false) WITHOUT scanning — zero cost. When ON, it runs an AUTHORITATIVE sequential scan of this SSTable’s own Data.db for the key (never a heuristic inference from byte patterns — the no-heuristics mandate). If the scan FINDS the key, the oracle produced a false negative: cqlite.read.bloom.false_negatives increments once with this SSTable’s bounded cqlite.sstable.format, a warning is logged, and Ok(true) is returned. A genuine absence returns Ok(false) and never emits. Under a correct bloom/BTI-trie the counter stays 0.

Source§

impl SSTableReader

Source

pub fn partition_key_out_of_range(&self, key: &[u8]) -> bool

Return true iff key provably sorts OUTSIDE this SSTable’s authoritative [first_key, last_key] partition-key bound, so the partition is definitely absent and the caller may skip all presence work.

Returns false (cannot rule out) when no authoritative bound is available (Summary.db absent, e.g. a BTI reader) or an endpoint is empty. See the module docs for the no-false-miss contract: the comparison is in Cassandra token order (Murmur3 token, unsigned-byte tiebreak), inclusive at both ends.

Precondition: the SSTable is sorted by Cassandra’s Murmur3Partitioner (the only partitioner CQLite targets — Cassandra 5.0). See the module docs.

Source§

impl SSTableReader

Source

pub async fn scan( &self, table_id: &TableId, start_key: Option<&RowKey>, end_key: Option<&RowKey>, limit: Option<usize>, schema: Option<&TableSchema>, ) -> Result<Vec<(RowKey, ScanRow)>>

Scan a range of keys

§Arguments
  • table_id - The table to scan
  • start_key - Optional start key for range scan
  • end_key - Optional end key for range scan
  • limit - Optional limit on number of results
  • schema - Optional table schema for schema-aware parsing. When provided, enables accurate type detection and avoids heuristic-based parsing. Strongly recommended for Cassandra 5.0+ formats.
Source

pub async fn get_all_entries(&self) -> Result<Vec<(TableId, RowKey, ScanRow)>>

Get all entries in the SSTable.

§Tombstone contract (Issue #505)

This is a user-facing accessor: row tombstones are filtered out via Self::filter_tombstone and never appear in the returned entries. The underlying parse_block path emits Value::Tombstone(RowTombstone) for deleted rows, but those are suppressed here so callers see exactly the live rows (matching the previous Value::Null suppression behaviour).

The compaction k-way merger must instead use Self::iterate_all_partitions_for_compaction, which preserves Value::Tombstone entries (with their authoritative deletion timestamps) so that tombstone-shadowing semantics can be applied during the merge.

Source

pub fn scan_stream( self: Arc<Self>, table_id: TableId, start_key: Option<RowKey>, end_key: Option<RowKey>, schema: Option<TableSchema>, buffer_size: usize, ) -> Receiver<Result<(RowKey, ScanRow)>>

Streaming scan (issue #790): yield (RowKey, ScanRow) entries lazily through a bounded channel instead of materializing the whole result in a Vec. Live heap is bounded by buffer_size rows (plus the stitched data-section buffer) rather than growing O(rows).

Entries are yielded in on-disk order — token order for a single SSTable — matching the order of the materializing scan path. The bounded channel applies backpressure: parsing pauses when the consumer falls behind and stops entirely if the consumer is dropped.

In-flight bound (chunk-stitching SSTables): the windowed pipeline (issue #1143) materializes one confirmed partition at a time and batches its rows to amortize the cross-thread wake, so against a stalled consumer the resident (RowKey, ScanRow) count is the SUM of three inherent terms, not one constant: buffer_size (this channel) + max_partition_size (the one fully-materialized confirmed partition — a pre-existing #1156 windowed-scan term, inherent to any row-materializing partition scan) + MAX_INFLIGHT_BATCH_ROWS (the FIXED, BOUNDED amount the issue-#1143 batching subsystem may run ahead, regardless of buffer_size, holding even for buffer_size == 1). MAX_INFLIGHT_BATCH_ROWS bounds the batching subsystem alone, NOT the max_partition_size materialization term. Non-stitching SSTables parse a whole block before forwarding its rows, so the resident (RowKey, ScanRow) count is bounded by buffer_size + (one parsed block's entries).

Source

pub fn scan_stream_batched( self: Arc<Self>, table_id: TableId, start_key: Option<RowKey>, end_key: Option<RowKey>, schema: Option<TableSchema>, buffer_size: usize, ) -> Receiver<Result<Vec<(RowKey, ScanRow)>>>

Batched streaming scan (issue #1592, Epic F/F2): the additive companion to scan_stream whose channel item is a Vec BATCH of (RowKey, ScanRow) entries rather than a single entry. Forwarding one batch per channel send collapses the one-async-wake-per-row cost the internal windowed pipeline (issue #1143) was designed to amortize but the public forwarder then re-flattened away.

Order and content are identical to scan_stream: flattening the batches yields exactly the per-row stream. Backpressure is preserved — the channel is bounded (in BATCHES) and every send observes it.

Source

pub async fn scan_with_cell_metadata( &self, table_id: &TableId, start_key: Option<&RowKey>, end_key: Option<&RowKey>, limit: Option<usize>, schema: Option<&TableSchema>, ) -> Result<Vec<(RowKey, ScanRow, HashMap<String, CellWriteMetadata>)>>

Scan a range of keys AND return per-cell write metadata.

Used when ProjectionFlags::include_cell_metadata is set (issue #693). Falls through to stitch_and_parse_all_chunks_with_metadata for V5CompressedLegacy format (the common path for real SSTables). Returns None as the metadata for non-V5 formats (they do not carry per-cell timestamps in a way the parser currently surfaces).

Source§

impl SSTableReader

Source

pub fn chunk_cache(&self) -> &Arc<DecompressedChunkCache>

The shared decompressed-chunk cache this reader consults (issue #1567).

Exposed so callers/tests can observe cache residency and per-instance hit/miss counts (parallelism-immune, unlike the process-global decompress_call_count).

Source

pub fn decompress_call_count() -> u64

Process-global count of actual chunk decompressions at the wired read sites (issue #1567). Tests reset around a cold/warm read pair and assert a warm-read delta of 0 to prove the hit skipped decompression.

Source

pub fn reset_decompress_calls()

Reset the decompress-work counter to zero (test/instrumentation harness).

Source

pub fn chunk_read_call_count() -> u64

Process-global count of compressed-bytes reads at the BIG point-read site (issue #1567). A warm point read that hits the cache leaves this unchanged.

Source

pub fn reset_chunk_read_calls()

Reset the chunk-read counter to zero (test/instrumentation harness).

Source

pub async fn get( &self, table_id: &TableId, key: &RowKey, ) -> Result<Option<ScanRow>>

Get a value by key from the SSTable.

Resolution-mode-agnostic entry point: callers that do not carry the manager’s resolve_reader_list signal (e.g. the per-reader helpers in partition_lookup, schema_aware_reader, and benchmarks) get the STRICT table-consistency guard — fully_qualified_match = false reproduces exactly today’s table_ids_match_strict behavior on the BTI point-lookup path, so this is a behavior-preserving conservative default. The manager’s get() calls SSTableReader::get_with_resolution with the authoritative signal so an exact fully-qualified match can accept rows across a benign header-keyspace divergence (issue #1321, mirroring the seek path #1284).

Source

pub async fn get_with_resolution( &self, table_id: &TableId, key: &RowKey, fully_qualified_match: bool, ) -> Result<Option<ScanRow>>

Get a value by key, threading the authoritative resolution mode (fully_qualified_match) into the BTI point-lookup guard (issue #1321).

See SSTableReader::get for the resolution-mode contract. Only the BTI (“da”) point-lookup path consults fully_qualified_match; the bloom/Index.db/ sequential fallbacks are unaffected by it.

Source

pub async fn read_value_at_offset( &self, offset: u64, size: u32, ) -> Result<Option<ScanRow>>

Read value at a specific offset with caching

Source

pub async fn prepare_delta_scan( &self, ) -> Result<(Vec<u8>, V5CompressedLegacyParser)>

Prepare for a delta-scan pass: stitch all compressed chunks of the data section and return the decompressed buffer together with a pre-configured parser.

Uses its own per-scan cursor (issue #815), so it no longer needs the caller to serialize against concurrent reads. This method is gated on the delta-scan feature and is the only bridge between the SSTableReader internals and the delta_scan module, which cannot access private helpers directly.

The schema parameter is not used here — it is threaded through the caller’s parse_block_emit_delta invocation instead. The parser is built via build_v5_parser() which handles version-gates and UDT registry without needing the schema at construction time.

Source§

impl SSTableReader

Source

pub async fn get_health_metrics(&self) -> Result<SSTableReaderHealthMetrics>

Get comprehensive reader health and performance metrics

Source

pub async fn perform_integrity_check(&self) -> Result<IntegrityCheckResult>

Perform an integrity check on the SSTable.

Issue #1283: this is a THIN PROJECTION over verify::verify_sstable — the single source of truth for SSTable integrity — not an independent check pipeline. The legacy implementation walked only Data.db blocks, so a corrupt Index.db / Digest.crc32 / Summary.db / Filter.db or out-of-order keys (all of which the verifier FAILs) read back Healthy here — a divergent verdict. We now run the authoritative verifier in Full mode over the reader’s EXACT generation (self.file_path, not merely its parent directory — roborev #1283) and map its VerifyReport onto the legacy IntegrityCheckResult shape the (test-only) consumers expect.

Source§

impl SSTableReader

Source

pub fn invalidate_key_cache_entries(&self) -> u64

Drop ALL of this reader’s generation’s entries from the process-global key cache (issue #2059 §C). Called when the generation is removed / compacted away / evicted from the warm registry, so its locations are reclaimed promptly and recorded on the distinct invalidations counter (never the budget-driven evictions). Returns the number of entries dropped. A #2383 rebind does NOT call this — the identity is unchanged across a rebind, so entries survive. No-op when the identity is None.

pub so the flight WarmTableRegistry can invalidate on warm eviction (issue #2059 §C) — the warm registry is the process’s largest reader pin, so its evictions are the primary reclamation trigger for the global cache.

Source

pub async fn lookup_partition_with_index( &self, partition_key: &[u8], ) -> Result<Option<(u64, u32)>>

Enhanced partition lookup using Index.db reader with promoted index support.

partition_key must be the raw partition-key bytes as produced by PartitionKey::to_bytes:

  • Single-component keys — raw value bytes (UUID = 16 bytes, int = 4 BE bytes, etc.).
  • Multi-component (composite) keys[len: u16 BE][value bytes][0x00] per component, including a trailing 0x00 after the final component.

The Index.db key_lookup map is keyed on these exact raw bytes (set when the BIG-format parser was fixed in Issue #552). The old digest-based path (which caused every lookup to miss) has been removed. On a miss the function returns Ok(None) so callers can fall through to their existing sequential-scan fallback.

Source

pub fn lookup_partition_via_bti_trie( &self, partition_key: &[u8], ) -> Result<Option<u64>>

BTI (“da”) partition point lookup via the in-memory Partitions.db trie (issue #831, building on the verified #755 primitive).

Encodes partition_key into the BTI byte-comparable trie key and walks the trie to resolve the partition’s location. Returns:

  • Ok(Some(offset)) — the UNCOMPRESSED Data.db byte offset of the partition. INVARIANT: this offset indexes into the DECOMPRESSED data section, never raw file bytes. The offset is resolved in one of two ways:
    • NARROW partition (BtiPartitionLocation::DataOffset) — the trie returns the Data.db offset directly.
    • WIDE partition (BtiPartitionLocation::RowsOffset) — the trie returns a positive offset into Rows.db; we deserialize that partition’s TrieIndexEntry via resolve_rows_db_entry and use its recovered data_position (issue #909/#910). Both forms share the same uncompressed Data.db offset domain, so the caller treats them identically.
  • Ok(None) — the reader is not BTI, or the key has no trie path (the partition is definitely absent from this SSTable).
  • Err(_) — a structural trie parse error, or a RowsOffset was returned without an accompanying Rows.db (a structurally invalid BTI SSTable).

Because the BTI trie uses path compression, a returned offset may be a candidate for a prefix-colliding key. The caller (bti_point_lookup) MUST verify the partition-key bytes at the resolved offset equal the queried key before returning rows.

Source

pub fn lookup_partition_via_bti_trie_encoded( &self, partition_key: &[u8], encoded: &[u8; 9], ) -> Result<Option<u64>>

Resolve a BTI partition using a PRE-ENCODED byte-comparable key, so the Murmur3 hash + encoding is computed once per read and reused across every candidate SSTable’s prune instead of being recomputed per candidate (issue #1575 / C4). encoded MUST equal encode_partition_key_for_bti_trie(partition_key); the caller (SSTableManager’s candidate prune) computes it exactly once. Semantics are otherwise identical to [lookup_partition_via_bti_trie]: same same-key memo, same presence-oracle observability, same resolved offset. A non-BTI reader returns Ok(None) (the encoded argument is simply unused).

Source

pub fn might_contain_partition(&self, partition_key: &[u8]) -> bool

Cheap presence oracle: can this SSTable possibly contain partition_key?

Used to prune SSTables before a partition-targeted scan (the query engine’s WHERE pk = ? fast path). Returning false MUST be definitive — the SSTable is then skipped entirely without being parsed — so this only ever returns false for an authoritative “absent” signal:

  • BTI (“da”) readers have no bloom filter; the Partitions.db trie is the authoritative present/absent oracle. A trie miss (Ok(None)) is definitive absence. A trie hit may be a prefix-collision candidate, which is a safe false positive here (the partition scan re-verifies the key). Any trie parse error is treated conservatively as “maybe present”.
  • BIG-format readers consult the bloom filter, which never reports false negatives: might_contain == false is definitive absence. With no bloom filter loaded we cannot prune, so we conservatively return true.

partition_key must be the raw partition-key bytes (same encoding the bloom filter and Index.db/BTI trie are keyed on).

Source

pub fn is_bti(&self) -> bool

true when this reader was opened on a BTI (“da”) SSTable (its Partitions.db trie is resident). Used by the candidate-prune loop to decide whether to hoist the BTI key encoding once per read (issue #1575 / C4).

Source

pub fn might_contain_partition_encoded( &self, partition_key: &[u8], encoded: &[u8; 9], ) -> bool

Candidate-prune presence check using a PRE-ENCODED BTI byte-comparable key, so a multi-generation WHERE pk = ? read hashes+encodes the key ONCE (the encoding is identical for every candidate) instead of once per candidate (issue #1575 / C4).

encoded MUST equal encode_partition_key_for_bti_trie(partition_key), computed once by the caller. For a BTI reader this consults the trie with the pre-encoded key (no re-hash); for a BIG reader (no Partitions.db) it falls back to the raw-key [might_contain_partition] path (the bloom filter is keyed on the raw key — there is no BTI encoding to hoist), so a mixed or non-BTI candidate set stays correct. Semantics match [might_contain_partition]: false is definitive absence; a BTI trie hit may be a safe prefix-collision false positive re-verified by the partition scan; any trie parse error is treated conservatively as “maybe present”.

Source

pub async fn iterate_all_partitions(&self) -> Result<Vec<(RowKey, ScanRow)>>

Enhanced partition iteration using Summary.db reader

Note: Token-based range queries are not directly supported because Summary.db does not store token values (Issue #218). Instead, this iterates all summary entries and returns all partition data.

For token-based filtering, compute tokens from partition keys after retrieval.

§Issue #500: Sequential-scan fallback for writer-produced SSTables

The Summary.db → Index.db → Data.db lookup path depends on Index.db format compatibility between writer and reader (digest format vs. raw-key format). Locally written SSTables emit raw-key Index.db entries that the reader’s digest-based parser cannot resolve, so the lookup loop returns 0 entries even though Summary.db enumerates the partitions correctly.

When that happens we fall back to sequential_scan, which walks Data.db directly. For V5CompressedLegacy NB SSTables (the format the writer emits), sequential_scan uses the chunk-stitching path and returns every partition.

Source

pub fn has_partition_index(&self) -> bool

Whether this reader has a usable random-access partition index — a loaded Index.db (BIG format; issue #2302 resolves CQLite-written Index.db too) or a Partitions.db trie (BTI format) — that lets iterate_all_partitions and point lookups avoid a full sequential Data.db scan.

This is a capability probe: it must report true only for the exact states that route to the non-sequential path. Both point lookups (index_reader.lookup_partition / the BTI trie) and iterate_all_partitions (the full-Index.db walk / BTI) gate on index_reader/bti_partitions_db — never on summary_reader.

Issue #2302 (roborev job 1613): summary_reader is DELIBERATELY excluded. A Summary.db only samples ~1-in-128 partitions and never gates the random-access route, so a present-Summary / absent-Index BIG reader is degraded — every read falls into sequential_scan and WARNs. Including summary_reader here made the probe report “fast path available” for that degraded reader (a fidelity lie). Narrowing to the truly-supporting state keeps the probe honest.

Snapshot-completeness probe (issue #2295): a directory served with only Data.db (its index siblings absent) still opens but reports false; a complete component set loads Index.db (→ index_reader) and reports true. All current callers (this module’s own tests + the #2295 snapshot test) use this probe to mean “does the fast/index path apply,” so a single narrowed predicate covers every caller without a split API.

Source

pub async fn iterate_token_range( &self, _start_token: i64, _end_token: i64, ) -> Result<Vec<(RowKey, ScanRow)>>

👎Deprecated since 0.1.0:

Summary.db does not store tokens. Use iterate_all_partitions() and filter by computed tokens.

Token range iteration (deprecated - tokens not stored in Summary.db)

This method is kept for API compatibility but simply delegates to iterate_all_partitions() since Summary.db does not store token values. Token filtering should be done by the caller after retrieval.

Source

pub async fn get_timestamp_range(&self) -> Result<Option<(i64, i64)>>

Get min/max timestamps from Statistics.db reader

Source

pub async fn get_token_coverage(&self) -> Result<Option<(i64, i64)>>

👎Deprecated since 0.1.0:

Summary.db does not store tokens. Compute tokens from partition keys using the partitioner.

Get token coverage (deprecated - tokens not stored in Summary.db)

Note: As of Issue #218, Summary.db does not store token values. This method now returns None since token coverage cannot be determined from Summary.db alone. Token computation requires partition keys and the partitioner algorithm.

Source§

impl SSTableReader

Source

pub async fn locate(&self, partition_key: &[u8]) -> Result<Option<(u64, u32)>>

Resolve partition_key to its uncompressed Data.db offset (and size, where the format records one) through the one format-tagged façade.

Returns:

  • Ok(Some((offset, size))) — the partition’s uncompressed data-section offset. size is the Index.db-recorded partition size for BIG, and 0 for BTI (the trie records no size). Callers keep treating size == 0 as “parse forward from the offset, do not range-read”.
  • Ok(None) — absent: out of the authoritative [first_key, last_key] bound (C5), a BTI trie miss (definitive absence), or a BIG Index.db miss (which the BIG point path treats as inconclusive and re-checks via a scan; see the carve-out above — an index miss is NOT a definitive absent).
  • Err(_) — the same typed Error::Corruption the underlying legacy path raises (a corrupt trie, a RowsOffset with no Rows.db, etc.).

Resolution order:

  1. C5 range short-circuit (step 1): if partition_key provably sorts outside this SSTable’s authoritative Summary bound, record one RANGE_SHORT_CIRCUITS and return absence before any presence work. A no-op when no bound exists (BTI / no Summary).
  2. Format dispatch: BTI (da) walks the Partitions.db trie; BIG (nb/uncompressed) probes the raw-key Index.db map. Both reuse the B4 key-offset cache and their existing presence-counter emissions.
Source§

impl SSTableReader

Source

pub async fn resolve_registry_schema(&mut self)

Pre-resolve the registry schema into the sync cache (issue #1692, AG3).

Call this from an async wiring point (e.g. open_reader_with_schema, or a DIRECT caller’s own async context), AFTER set_schema_registry — or use the combined attach_schema_registry. It resolves the table schema from the async registry a single time — properly awaited — and caches it in self.registry_schema, so the SYNC schema-fallback tier of get_table_schema reads a plain field instead of block_on-ing the async registry lock on a tokio worker thread.

Cold path only: when the SSTable header already carried a schema (self.schema.is_some()) the sync path short-circuits before the registry tier, so no resolution is performed. A registry miss leaves the cache None; the sync path then falls through to header-column construction, exactly as the previous block_on path did on a miss.

Source§

impl SSTableReader

Source

pub async fn open( path: &Path, config: &Config, platform: Arc<Platform>, ) -> Result<Self>

Open an SSTable file for reading.

Instrumented (epic #1031 / #1034): wraps the open in a sstable.reader.open span, increments the SSTABLES_OPEN gauge on success, and records an error on the reader subsystem when open fails.

Source

pub async fn open_cancellable( path: &Path, config: &Config, platform: Arc<Platform>, cancel: ScanCancel, ) -> Result<Self>

Cancel-aware open (issue #2383 fix C).

Threads a synchronous ScanCancel into the Index.db partition-index parse so a client-disconnect cancel aborts a 1.58M-entry parse within one poll interval instead of pinning a tokio worker to completion. Non-cancellable callers use open (a default never-cancel flag). Returns Error::Cancelled on a mid-parse trip.

Source

pub async fn open_with_cache( path: &Path, config: &Config, platform: Arc<Platform>, cache: Arc<DecompressedChunkCache>, ) -> Result<Self>

Open an SSTable file for reading, sharing the provided DecompressedChunkCache.

Identical to open except the reader stores cache (an Arc clone) instead of minting its own, so every reader a manager opens for one dataset consults the same bytes-bounded chunk cache (issue #1567).

Source

pub async fn open_with_cache_cancellable( path: &Path, config: &Config, platform: Arc<Platform>, cache: Arc<DecompressedChunkCache>, cancel: ScanCancel, ) -> Result<Self>

Cancel-aware open_with_cache (issue #2383 fix C): same as it but threads cancel into the Index.db parse. See open_cancellable.

Source

pub fn set_schema_registry( &mut self, schema_registry: Arc<RwLock<SchemaRegistry>>, )

👎Deprecated:

attaches the registry but CANNOT pre-resolve the sync schema-fallback cache (would need a forbidden block_on, issue #1692); registry schemas will not be available to a subsequent sync get_table_schema. Use the async attach_schema_registry (which pre-resolves), or call resolve_registry_schema after this from an async context, for registry-schema-aware reads.

Set the schema registry for schema-driven operations.

This is a SYNC method (&mut self, non-async), so it CANNOT await the async registry to pre-resolve the sync schema-fallback cache (registry_schema) — doing so would require a block_on, which #1692 (AG3) forbids on a tokio worker thread. It therefore only stores the registry and INVALIDATES any previously pre-resolved cache (the new registry may resolve a different schema).

Direct callers that intend to trigger a SYNC parse (whose schema-fallback tier reads registry_schema) must, from an async context, either call the combined attach_schema_registry instead, or call resolve_registry_schema after this. Otherwise the sync path deliberately falls through to header-column construction (or None). The crate’s own async wiring path (open_reader_with_schema) already does this.

Source

pub async fn attach_schema_registry( &mut self, schema_registry: Arc<RwLock<SchemaRegistry>>, )

Attach a schema registry AND pre-resolve it into the sync fallback cache (issue #1692). Async wiring convenience for DIRECT callers: it combines set_schema_registry with resolve_registry_schema so the sync get_table_schema fallback tier is populated without any block_on.

Prefer this over the bare sync set_schema_registry whenever you attach a registry to a reader you will parse synchronously.

Source

pub fn set_udt_registry(&mut self, registry: UdtRegistry)

Set the UDT registry for UDT-aware parsing in collections

This enables proper parsing of UDTs inside collections (List, Set, Map) by providing the UDT field definitions needed for nested type resolution.

Source

pub fn has_udt_registry(&self) -> bool

Whether a UDT registry has been wired onto this reader (issue #2310, WS1 #2345). A warm-handle cache that hands SHARED Arc<SSTableReader>s to the reader-based k-way merge seam (KWayMerger::new_from_readers, which takes NO udt_registry parameter) MUST open each reader WITH its registry already resolved before wrapping it in Arc; this getter lets that caller PROVE the registry is present rather than silently decoding a frozen/nested UDT cell as Blob (the #1234 data-loss class).

Source

pub fn file_path(&self) -> PathBuf

The Data.db path this reader was opened from (issue #2310). The warm registry keys parsed state on the file’s inode-stable generation identity, so it needs the backing path to stat device+inode without re-listing the directory.

Source

pub fn file_size(&self) -> u64

The Data.db size in bytes captured at open. Used by the warm registry’s authoritative rebind identity check (issue #2383): a rebind is accepted only when the live candidate’s (device, inode) AND size match this reader’s, else it fails closed to a full re-open.

Source

pub fn rebind_path(&self, new_path: &Path)

Rebind this reader’s lazy-scan path to new_path WITHOUT re-opening or re-parsing (issue #2383, the #2356 “rebind-by-inode” direction).

The caller MUST have already established that new_path is the SAME on-disk generation as this reader by AUTHORITATIVE identity — matching (device, inode) and size (issue #28 no-heuristics; Cassandra snapshot files are hardlinks to the immutable SSTable, so a same-inode candidate is byte-identical). The already-open point/scan handles reference the shared inode and stay valid across the swap; only Self::new_scan_cursor’s per-scan File::open reads this path, so pointing it at a LIVE hardlink restores the #2352 ENOENT protection with zero parse cost.

§In-flight isolation invariant (roborev-1654 HIGH, adjudicated)

Mutating this shared Arc<SSTableReader>’s path while a concurrent request scans it does NOT break the in-flight request’s isolation, because the rebind is byte-transparent and monotone-toward-live:

  1. Same immutable inode. The sole caller (warm::registry::rebind) fires only after rebuild::rebind_matches proves (device, inode, generation) + size equality; every rebind target is a hardlink to the SAME immutable SSTable inode, so any path this reader carries yields byte-identical data (issue #28 no-heuristics).
  2. Atomic swap. file_path is an arc_swap::ArcSwap<PathBuf>; a concurrent file_path() sees either the old or the new whole PathBuf, never a torn value.
  3. Dead → live only. A rebind happens only when the current path is dead and an identity-matching LIVE hardlink exists, so an in-flight request’s NEXT new_scan_cursor File::open is strictly MORE likely to succeed after the rebind than before (pre-rebind it would ENOENT).
  4. No semantics off the path. No file_path() consumer re-derives keyspace/table/schema from the path after open; the path is used ONLY for File::open (bytes) and all parsed read state (header, compression info, CRC, index, generation) lives on &self, fixed at open — the swapped path changes which hardlink is read, never what the bytes mean. This covers BOTH the Data.db path AND the lazy Index.db path (below): both are same-inode hardlink siblings in the rebound snapshot dir.
§Rebinding the lazy Index.db path too (issue #2356 roborev)

Repointing ONLY the Data.db path reintroduced the #2352 ENOENT class for the dominant point-read shape: a BIG reader opened lazily over a usable Summary.db (#2412 §A) keeps its own Index.db path, and every DEFERRED Index.db open (ensure_materialized, the Summary-guided bounded-interval point probe) reads THAT path. If the original snapshot dir is torn down before the reader ever materialized, a not-yet-materialized reader would File::open the dead path and ENOENT. So the rebind ALSO repoints the IndexReader’s path to the Index.db hardlink sibling of new_path, keeping every Index.db consumer (deferred-materialize, point-interval, and the streaming walk’s current_index_db_path Data.db-sibling derivation) on the live generation.

Source

pub fn endpoint_tokens(&self) -> Option<(i64, i64)>

The immutable [first_key_token, last_key_token] endpoints cached at open (issue #1576), or None when Summary.db was absent/empty. The Flight warm registry (issue #2310) uses this to token-prune a WARM reader set with ZERO extra I/O — a warm hit re-reads no Summary.db, preserving the “zero Index/Summary/Statistics/bloom parse” property even for a token-filtered scan. SSTables store partitions in token order, so the first endpoint is the min token and the last the max.

Source

pub fn index_is_materialized(&self) -> bool

Whether this reader’s Index.db partition map is CURRENTLY fully resident (issue #2412 §D — the Flight warm registry’s memory accounting).

A BIG reader opened lazily over a usable Summary.db (design §A) reports false until some consumer’s ensure_materialized-driven full parse (a full/compaction scan whose Summary-guided streaming walk FellBack) actually happens; the Summary-guided point/scan paths (§B/§C) never trigger it. An eagerly-opened reader (the Summary.db-absent FellBack case, §A1) reports true immediately — its Index.db was fully parsed at open. No Index.db at all (absent component) reports true (there is no resident cost to represent either way).

Source

pub fn set_scan_cancel(&mut self, cancel: ScanCancel)

Wire a cooperative-cancellation token into this reader’s long-running scans (issue #2264).

The compaction streaming read and its sequential-scan fallback poll this token at a bounded interval, so a cancelled Flight do_get abandons an otherwise uninterruptible full-Data.db walk within milliseconds instead of waiting out the coarse ~1–2 min transport backstop. Idempotent; the last token set wins. Readers never wired keep the default never-cancel flag.

Source

pub async fn stats(&self) -> Result<&SSTableReaderStats>

Get reader statistics

Source

pub async fn close(self) -> Result<()>

Close the reader and release resources

Source

pub fn calculate_header_size(&self) -> usize

Calculate header size based on format and actual header content

Source

pub fn cassandra_version(&self) -> CassandraVersion

Get the Cassandra version from the SSTable header

Source

pub fn has_uint_deletion_time(&self) -> bool

Whether the on-disk DeletionTime uses the Cassandra 5.0 UNSIGNED localDeletionTime serializer (oa/da, hasUIntDeletionTime) rather than the legacy SIGNED i32 form (nb).

The verifier uses this to interpret a raw partition-level localDeletionTime: on the legacy signed form a NEGATIVE value (that is not the i32::MAX live sentinel) is unambiguously corrupt, whereas on the unsigned form a value in [2^31, 2^32) is a legitimate far-future deletion time and must NOT be flagged (no heuristics — the format decides).

Authority: BigFormat.java:409 (hasUIntDeletionTime), DeletionTime.java.

Source

pub fn format_version(&self) -> Result<String>

Get the SSTable format version string

Source

pub fn header(&self) -> &SSTableHeader

Get a reference to the SSTable header

Source

pub fn schema(&self) -> Option<&TableSchema>

Get the table schema extracted from the SSTable header

Returns None for legacy formats or if schema extraction failed.

Source

pub fn effective_schema(&self) -> Option<TableSchema>

The effective table schema the read/verify paths decode with — the same four-tier resolution partition_clustering_verify_scan uses (issue #1282).

Returned owned because the registry-fallback tier constructs a schema; the verifier needs it to drive the authoritative clustering comparator (crate::storage::write_engine::mutation::ClusteringKey::compare).

Source

pub fn extract_write_time_from_entry(&self, _key: &RowKey, row: &ScanRow) -> i64

Extract write time from entry metadata

Trait Implementations§

Source§

impl Debug for SSTableReader

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for SSTableReader

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more