use super::super::access_path::FallbackReason;
use super::PartitionLookupOutcome;
use crate::config::ReadPathMode;
use crate::{Error, Result};
use std::sync::OnceLock;
const READ_PATH_ENV: &str = "CQLITE_READ_PATH";
pub(super) fn parse_read_path_mode(raw: &str) -> Result<ReadPathMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(ReadPathMode::Auto),
"point" => Ok(ReadPathMode::Point),
"full" => Ok(ReadPathMode::Full),
_ => Err(Error::invalid_read_path(raw)),
}
}
pub(super) fn resolve_mode(
config_override: Option<ReadPathMode>,
env_value: Option<&str>,
) -> Result<ReadPathMode> {
if let Some(mode) = config_override {
return Ok(mode);
}
match env_value {
Some(raw) => parse_read_path_mode(raw),
None => Ok(ReadPathMode::Auto),
}
}
fn cached_env() -> Option<&'static str> {
static ENV: OnceLock<Option<String>> = OnceLock::new();
ENV.get_or_init(|| std::env::var(READ_PATH_ENV).ok())
.as_deref()
}
pub(super) fn resolve_read_path_mode(
config_override: Option<ReadPathMode>,
) -> Result<ReadPathMode> {
resolve_mode(config_override, cached_env())
}
#[derive(Debug)]
pub(super) enum ForcedPlan {
Proceed(PartitionLookupOutcome),
ForceFullScan,
}
pub(super) fn apply_forcing(
outcome: PartitionLookupOutcome,
mode: ReadPathMode,
) -> Result<ForcedPlan> {
match mode {
ReadPathMode::Auto => Ok(ForcedPlan::Proceed(outcome)),
ReadPathMode::Full => Ok(ForcedPlan::ForceFullScan),
ReadPathMode::Point => match outcome {
PartitionLookupOutcome::Fallback(reason) => {
Err(Error::forced_read_path_unavailable("point", reason.label()))
}
targeted => Ok(ForcedPlan::Proceed(targeted)),
},
}
}
pub(super) fn point_requires_engaged(
mode: ReadPathMode,
engaged: bool,
reason: FallbackReason,
) -> Result<()> {
if mode == ReadPathMode::Point && !engaged {
return Err(Error::forced_read_path_unavailable("point", reason.label()));
}
Ok(())
}
pub(super) fn point_forbids_fallback(mode: ReadPathMode, reason: FallbackReason) -> Result<()> {
if mode == ReadPathMode::Point {
return Err(Error::forced_read_path_unavailable("point", reason.label()));
}
Ok(())
}
pub(super) fn full_forbids_schemaless_seek(mode: ReadPathMode, seek_eligible: bool) -> Result<()> {
if mode == ReadPathMode::Full && seek_eligible {
return Err(Error::forced_read_path_unavailable(
"full",
"schema_less_sole_pk_lookup_requires_targeted_seek",
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_is_case_insensitive_and_trims() {
assert_eq!(parse_read_path_mode("auto").unwrap(), ReadPathMode::Auto);
assert_eq!(parse_read_path_mode("POINT").unwrap(), ReadPathMode::Point);
assert_eq!(parse_read_path_mode(" Full ").unwrap(), ReadPathMode::Full);
}
#[test]
fn parse_rejects_unknown_value_loudly() {
let err = parse_read_path_mode("compact").expect_err("unknown value must error");
let msg = err.to_string();
assert!(
msg.contains("compact"),
"error must name the invalid value: {msg}"
);
assert!(
msg.contains("auto") && msg.contains("point") && msg.contains("full"),
"error must name the allowed set: {msg}"
);
assert!(matches!(err, Error::InvalidReadPath { .. }));
}
#[test]
fn resolve_unset_is_auto() {
assert_eq!(resolve_mode(None, None).unwrap(), ReadPathMode::Auto);
}
#[test]
fn resolve_reads_env_when_no_config() {
assert_eq!(
resolve_mode(None, Some("point")).unwrap(),
ReadPathMode::Point
);
assert_eq!(
resolve_mode(None, Some("full")).unwrap(),
ReadPathMode::Full
);
}
#[test]
fn resolve_config_overrides_env() {
assert_eq!(
resolve_mode(Some(ReadPathMode::Point), Some("full")).unwrap(),
ReadPathMode::Point
);
assert_eq!(
resolve_mode(Some(ReadPathMode::Auto), Some("bogus")).unwrap(),
ReadPathMode::Auto
);
}
#[test]
fn resolve_invalid_env_without_config_errors() {
let err = resolve_mode(None, Some("bogus")).expect_err("invalid env must error");
assert!(matches!(err, Error::InvalidReadPath { .. }));
}
#[test]
fn apply_forcing_auto_passes_through() {
let plan = apply_forcing(
PartitionLookupOutcome::Targeted(vec![1, 2, 3]),
ReadPathMode::Auto,
)
.unwrap();
assert!(matches!(
plan,
ForcedPlan::Proceed(PartitionLookupOutcome::Targeted(_))
));
}
#[test]
fn apply_forcing_full_forces_full_scan_for_any_outcome() {
for outcome in [
PartitionLookupOutcome::Targeted(vec![9]),
PartitionLookupOutcome::MultiTargeted(vec![vec![1], vec![2]]),
PartitionLookupOutcome::Fallback(FallbackReason::NoSchema),
] {
let plan = apply_forcing(outcome, ReadPathMode::Full).unwrap();
assert!(matches!(plan, ForcedPlan::ForceFullScan));
}
}
#[test]
fn apply_forcing_point_errors_on_fallback() {
let err = apply_forcing(
PartitionLookupOutcome::Fallback(FallbackReason::PartitionKeyNotFullyConstrained),
ReadPathMode::Point,
)
.expect_err("point on a classification fallback must fail closed");
match err {
Error::ForcedReadPathUnavailable { forced, reason } => {
assert_eq!(forced, "point");
assert_eq!(reason, "partition_key_not_fully_constrained");
}
other => panic!("expected ForcedReadPathUnavailable, got {other:?}"),
}
}
#[test]
fn apply_forcing_point_proceeds_on_targeted() {
let plan = apply_forcing(
PartitionLookupOutcome::Targeted(vec![7]),
ReadPathMode::Point,
)
.unwrap();
assert!(matches!(
plan,
ForcedPlan::Proceed(PartitionLookupOutcome::Targeted(_))
));
}
#[test]
fn point_requires_engaged_errors_only_when_not_engaged_under_point() {
let err = point_requires_engaged(
ReadPathMode::Point,
false,
FallbackReason::TombstonesBuildNoPrune,
)
.expect_err("point + !engaged must fail closed");
match err {
Error::ForcedReadPathUnavailable { reason, .. } => {
assert_eq!(reason, "tombstones_build_no_prune")
}
other => panic!("expected ForcedReadPathUnavailable, got {other:?}"),
}
assert!(point_requires_engaged(
ReadPathMode::Point,
true,
FallbackReason::TombstonesBuildNoPrune
)
.is_ok());
assert!(point_requires_engaged(
ReadPathMode::Auto,
false,
FallbackReason::TombstonesBuildNoPrune
)
.is_ok());
assert!(point_requires_engaged(
ReadPathMode::Full,
false,
FallbackReason::TombstonesBuildNoPrune
)
.is_ok());
}
#[test]
fn point_forbids_fallback_errors_only_under_point() {
assert!(
point_forbids_fallback(ReadPathMode::Point, FallbackReason::MetadataScanPath).is_err()
);
assert!(
point_forbids_fallback(ReadPathMode::Auto, FallbackReason::MetadataScanPath).is_ok()
);
assert!(
point_forbids_fallback(ReadPathMode::Full, FallbackReason::MetadataScanPath).is_ok()
);
}
#[test]
fn full_forbids_schemaless_seek_errors_only_under_full_when_eligible() {
let err = full_forbids_schemaless_seek(ReadPathMode::Full, true)
.expect_err("forced full on a schema-less sole-pk seek must fail closed");
match err {
Error::ForcedReadPathUnavailable { forced, reason } => {
assert_eq!(forced, "full");
assert_eq!(reason, "schema_less_sole_pk_lookup_requires_targeted_seek");
}
other => panic!("expected ForcedReadPathUnavailable, got {other:?}"),
}
assert!(full_forbids_schemaless_seek(ReadPathMode::Full, false).is_ok());
assert!(full_forbids_schemaless_seek(ReadPathMode::Auto, true).is_ok());
assert!(full_forbids_schemaless_seek(ReadPathMode::Point, true).is_ok());
}
}