#![cfg(feature = "write-support")]
use std::collections::HashMap;
use std::sync::Arc;
use cqlite_core::config::{DiskAccessMode, PrefetchMode};
use cqlite_core::schema::{Column, KeyColumn, TableSchema};
use cqlite_core::storage::scan_cancel::ScanCancel;
use cqlite_core::storage::sstable::reader::SSTableReader;
use cqlite_core::storage::sstable::writer::SSTableWriter;
use cqlite_core::storage::write_engine::mutation::{
CellOperation, Mutation, PartitionKey, TableId as MutationTableId,
};
use cqlite_core::types::{TableId, Value};
use cqlite_core::{Config, Platform};
use tempfile::TempDir;
const POINT_MMAP_THRESHOLD: u64 = 8 * 1024 * 1024;
const CELL_BYTES: usize = 512 * 1024;
const PARTITIONS: usize = 24;
fn declared_gaps() -> &'static [&'static str] {
&[
"iterate_all_partitions_cancellable (pub(crate)) — reached only via \
iterate_all_partitions; covered by nesting",
"iterate_all_partitions_via_full_index (pub(in reader)) — reached via \
iterate_all_partitions_cancellable; covered by nesting",
"stream_partitions_summary_guided / _compaction (pub(in reader) / private) — \
reached via stream_all_partitions_for_query; covered by nesting",
"stream_all_partitions_via_full_index / stream_all_partitions_cancellable \
(pub(in reader)) — not reachable from outside the crate; wired and \
compile-checked only",
"bti_scan_with_metadata / _cancellable / stream_bti_scan (pub(super)) — BTI \
(`da`) only, and no BTI writer path produces an >= 8 MiB fixture in this \
test; wired and compile-checked only, NOT behaviourally covered",
"sequential_scan (pub(in reader)) — reached via scan()/iterate_all_partitions \
on a reader with no usable index; covered by nesting",
]
}
fn schema() -> TableSchema {
TableSchema {
keyspace: "test_ks".to_string(),
table: "scan_lifetime".to_string(),
partition_keys: vec![KeyColumn {
name: "id".to_string(),
data_type: "int".to_string(),
position: 0,
}],
clustering_keys: vec![],
columns: vec![
Column {
name: "id".to_string(),
data_type: "int".to_string(),
nullable: false,
default: None,
is_static: false,
},
Column {
name: "payload".to_string(),
data_type: "text".to_string(),
nullable: true,
default: None,
is_static: false,
},
],
comments: HashMap::new(),
dropped_columns: HashMap::new(),
}
}
fn write_fixture(temp: &TempDir, partitions: usize, cell_bytes: usize) -> std::path::PathBuf {
let schema = schema();
let mut writer = SSTableWriter::new(temp.path().to_path_buf(), 1, &schema).expect("writer");
let payload = "x".repeat(cell_bytes);
let mut rows: Vec<_> = (0..partitions as i32)
.map(|i| {
let m = Mutation::new(
MutationTableId::new("test_ks", "scan_lifetime"),
PartitionKey::single("id", Value::Integer(i)),
None,
vec![CellOperation::Write {
column: "payload".to_string(),
value: Value::text(payload.clone()),
}],
1_000_000,
None,
);
let key = m.decorated_key(&schema).expect("decorated key");
(key, m)
})
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
for (key, m) in rows {
writer
.write_partition(key, vec![m])
.expect("write partition");
}
let info = futures::executor::block_on(writer.finish()).expect("finish");
info.data_path
}
fn config(mode: DiskAccessMode, prefetch: PrefetchMode) -> Config {
let mut config = Config::default();
config.storage.disk_access_mode = mode;
config.storage.prefetch = prefetch;
config
}
async fn open_reader(path: &std::path::Path, config: &Config) -> SSTableReader {
let platform = Arc::new(Platform::new(config).await.expect("platform"));
SSTableReader::open(path, config, Arc::clone(&platform))
.await
.expect("open reader")
}
async fn armed_reader(temp: &TempDir) -> SSTableReader {
let path = write_fixture(temp, PARTITIONS, CELL_BYTES);
let size = std::fs::metadata(&path).expect("stat").len();
assert!(
size >= POINT_MMAP_THRESHOLD,
"fixture must clear the #2210 point-mapping threshold to arm the seam \
({size} < {POINT_MMAP_THRESHOLD})"
);
let reader = open_reader(&path, &config(DiskAccessMode::Mmap, PrefetchMode::WillNeed)).await;
assert!(
reader.scan_lifetime_enabled(),
"POSITIVE CONTROL FAILED: the scan-lifetime seam is not armed, so every \
count assertion below would be vacuous. It is armed only for an \
mmap-backed reader (this proves the mmap backend was taken) at an \
explicit PrefetchMode::WillNeed whose point plane holds its own mapping."
);
reader
}
#[tokio::test]
async fn open_issues_no_advice() {
let temp = TempDir::new().expect("temp dir");
let reader = armed_reader(&temp).await;
assert_eq!(
reader.scan_lifetime_advice_counts(),
(0, 0),
"issue #3853: reader OPEN must issue no madvise — WillNeed is a \
scan-lifetime advice now"
);
assert_eq!(reader.scan_lifetime_in_flight(), 0);
}
fn assert_advised_once(label: &str, reader: &SSTableReader, rows: usize) {
assert!(
rows > 0,
"{label}: scanned a present fixture and got 0 rows"
);
assert_eq!(
reader.scan_lifetime_advice_counts(),
(1, 1),
"{label}: expected exactly one WILLNEED and one DONTNEED"
);
assert_eq!(
reader.scan_lifetime_in_flight(),
0,
"{label}: the guard must have released"
);
}
#[tokio::test]
async fn every_reachable_entry_point_advises_once_and_releases() {
for gap in declared_gaps() {
println!("DECLARED GAP (not behaviourally covered here): {gap}");
}
let temp = TempDir::new().expect("temp dir");
let table_id = TableId::new("test_ks.scan_lifetime");
{
let reader = armed_reader(&temp).await;
let rows = reader
.scan(&table_id, None, None, None, None)
.await
.expect("scan")
.len();
assert_advised_once("scan", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let rows = reader
.iterate_all_partitions()
.await
.expect("iterate")
.len();
assert_advised_once("iterate_all_partitions", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let rows = reader
.iterate_all_partitions_for_compaction(Some(&schema()))
.await
.expect("compaction iterate")
.len();
assert_advised_once("iterate_all_partitions_for_compaction", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let mut rows = 0usize;
let cancel = ScanCancel::new();
reader
.stream_all_partitions_for_query(Some(&schema()), &cancel, None, |_row| {
rows += 1;
Ok(std::ops::ControlFlow::Continue(()))
})
.await
.expect("summary-guided stream");
assert_advised_once("stream_all_partitions_for_query", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let rows = reader
.get_all_entries()
.await
.expect("get_all_entries")
.len();
assert_advised_once("get_all_entries", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let rows = reader
.scan_with_cell_metadata(&table_id, None, None, None, Some(&schema()))
.await
.expect("metadata scan")
.len();
assert_advised_once("scan_with_cell_metadata", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let rows = reader
.distinct_partition_keys()
.await
.expect("distinct keys")
.len();
assert_advised_once("distinct_partition_keys", &reader, rows);
}
{
let reader = armed_reader(&temp).await;
let mut rows = 0usize;
let cancel = ScanCancel::new();
reader
.stream_all_partitions_for_compaction(Some(&schema()), &cancel, |_row| {
rows += 1;
Ok(std::ops::ControlFlow::Continue(()))
})
.await
.expect("compaction stream");
assert_advised_once("stream_all_partitions_for_compaction", &reader, rows);
}
}
#[tokio::test]
async fn spawned_stream_scans_advise_once_and_release() {
let temp = TempDir::new().expect("temp dir");
let table_id = TableId::new("test_ks.scan_lifetime");
let reader = Arc::new(armed_reader(&temp).await);
let mut rx = Arc::clone(&reader).scan_stream(table_id.clone(), None, None, Some(schema()), 16);
let mut rows = 0usize;
while let Some(item) = rx.recv().await {
item.expect("streamed row");
rows += 1;
}
drop(rx);
assert!(rows > 0, "scan_stream returned 0 rows on a present fixture");
assert_eq!(reader.scan_lifetime_advice_counts(), (1, 1), "scan_stream");
assert_eq!(reader.scan_lifetime_in_flight(), 0, "scan_stream");
let reader = Arc::new(armed_reader(&temp).await);
let mut rx = Arc::clone(&reader).scan_stream_batched(table_id, None, None, Some(schema()), 16);
let mut rows = 0usize;
while let Some(item) = rx.recv().await {
rows += item.expect("streamed batch").len();
}
drop(rx);
assert!(rows > 0, "scan_stream_batched returned 0 rows");
assert_eq!(
reader.scan_lifetime_advice_counts(),
(1, 1),
"scan_stream_batched"
);
assert_eq!(reader.scan_lifetime_in_flight(), 0, "scan_stream_batched");
}
#[tokio::test]
async fn error_and_cancelled_scans_still_release() {
let temp = TempDir::new().expect("temp dir");
let reader = armed_reader(&temp).await;
let cancel = ScanCancel::new();
let result = reader
.stream_all_partitions_for_query(Some(&schema()), &cancel, None, |_row| {
Err(cqlite_core::Error::corruption("induced emit failure"))
})
.await;
assert!(result.is_err(), "the induced emit failure must propagate");
assert_eq!(
reader.scan_lifetime_advice_counts(),
(1, 1),
"an Err scan must still release"
);
assert_eq!(reader.scan_lifetime_in_flight(), 0);
let path = write_fixture(&temp, PARTITIONS, CELL_BYTES);
let mut reader =
open_reader(&path, &config(DiskAccessMode::Mmap, PrefetchMode::WillNeed)).await;
let cancel = ScanCancel::new();
reader.set_scan_cancel(cancel.clone());
assert!(reader.scan_lifetime_enabled(), "POSITIVE CONTROL FAILED");
cancel.cancel();
let result = reader.iterate_all_partitions().await;
assert!(result.is_err(), "a cancelled enumeration must return Err");
assert_eq!(
reader.scan_lifetime_in_flight(),
0,
"a cancelled scan must still release"
);
assert_eq!(
reader.scan_lifetime_advice_counts(),
(1, 1),
"a cancelled scan advises on entry and releases on drop"
);
}
#[tokio::test]
async fn point_plane_sharing_the_scan_mapping_disables_the_seam() {
let temp = TempDir::new().expect("temp dir");
let path = write_fixture(&temp, 2, 1024);
let size = std::fs::metadata(&path).expect("stat").len();
assert!(
size < POINT_MMAP_THRESHOLD,
"this case needs a sub-threshold fixture ({size} >= {POINT_MMAP_THRESHOLD})"
);
let reader = open_reader(&path, &config(DiskAccessMode::Mmap, PrefetchMode::WillNeed)).await;
assert!(
!reader.scan_lifetime_enabled(),
"a sub-8-MiB reader shares ONE mapping between the point and scan planes, \
so the seam must be disabled"
);
let rows = reader
.iterate_all_partitions()
.await
.expect("iterate")
.len();
assert!(rows > 0, "scanned a present fixture and got 0 rows");
assert_eq!(
reader.scan_lifetime_advice_counts(),
(0, 0),
"the shared-mapping reader must never release the scan mapping"
);
}
#[tokio::test]
async fn auto_prefetch_issues_no_advice_ever() {
let temp = TempDir::new().expect("temp dir");
let path = write_fixture(&temp, PARTITIONS, CELL_BYTES);
let table_id = TableId::new("test_ks.scan_lifetime");
for prefetch in [
PrefetchMode::Auto,
PrefetchMode::Off,
PrefetchMode::Sequential,
] {
let reader = open_reader(&path, &config(DiskAccessMode::Mmap, prefetch)).await;
assert!(
!reader.scan_lifetime_enabled(),
"prefetch {prefetch:?} must leave the scan-lifetime seam disabled \
(issue #1143 for Auto)"
);
let rows = reader
.scan(&table_id, None, None, None, None)
.await
.expect("scan")
.len();
assert!(
rows > 0,
"prefetch {prefetch:?}: 0 rows on a present fixture"
);
assert_eq!(
reader.scan_lifetime_advice_counts(),
(0, 0),
"prefetch {prefetch:?} must issue NO scan-lifetime madvise"
);
}
}
#[tokio::test]
async fn buffered_backend_never_advises() {
let temp = TempDir::new().expect("temp dir");
let path = write_fixture(&temp, PARTITIONS, CELL_BYTES);
let reader = open_reader(
&path,
&config(DiskAccessMode::Buffered, PrefetchMode::WillNeed),
)
.await;
assert!(!reader.scan_lifetime_enabled());
assert!(!reader
.iterate_all_partitions()
.await
.expect("iterate")
.is_empty());
assert_eq!(reader.scan_lifetime_advice_counts(), (0, 0));
}