use super::{Durability, Mutation, WriteEngine, WriteEngineConfig};
use crate::error::Error;
use crate::storage::write_engine::mutation::{CellOperation, PartitionKey, TableId};
use crate::storage::write_engine::test_support::{create_test_mutation, create_test_schema};
use crate::types::Value;
use tempfile::TempDir;
fn create_sized_mutation(id: i32, bytes: usize, timestamp: i64) -> Mutation {
let table_id = TableId::new("test_ks", "test_table");
let pk = PartitionKey::single("id", Value::Integer(id));
let ops = vec![CellOperation::Write {
column: "name".to_string(),
value: Value::text("x".repeat(bytes)),
}];
Mutation::new(table_id, pk, None, ops, timestamp, None)
}
#[test]
fn test_single_jumbo_mutation_rejected_into_empty_memtable() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_flush_threshold(1024 * 1024)
.with_hard_limit(64 * 1024) .with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
assert_eq!(engine.memtable_size(), 0);
let jumbo = create_sized_mutation(1, 128 * 1024, 1_000_000);
let err = engine.write(jumbo).unwrap_err();
match err {
Error::Storage(msg) => {
assert!(
msg.contains("single mutation") && msg.contains("hard limit"),
"expected distinct single-mutation ceiling error, got: {msg}"
);
}
other => panic!("expected Storage error, got: {other:?}"),
}
assert_eq!(engine.memtable_size(), 0);
assert_eq!(engine.memtable_row_count(), 0);
}
#[test]
fn test_projected_sum_over_hard_limit_rejected() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_flush_threshold(1024 * 1024)
.with_hard_limit(20 * 1024) .with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
engine
.write(create_sized_mutation(1, 12 * 1024, 1_000_000))
.unwrap();
let size_after_first = engine.memtable_size();
assert!(size_after_first >= 12 * 1024);
let err = engine
.write(create_sized_mutation(2, 12 * 1024, 1_000_001))
.unwrap_err();
match err {
Error::Storage(msg) => assert!(
msg.contains("would exceed hard limit"),
"expected projected-sum error, got: {msg}"
),
other => panic!("expected Storage error, got: {other:?}"),
}
assert_eq!(
engine.memtable_size(),
size_after_first,
"rejected write must not change memtable size"
);
assert_eq!(engine.memtable_row_count(), 1);
}
#[test]
fn test_admission_and_accounting_agree() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
let mutation = create_test_mutation(1, "a normal name", 1_000_000);
let predicted = engine.memtable.estimate_mutation_size(&mutation);
let before = engine.memtable_size();
engine.write(mutation).unwrap();
assert_eq!(
engine.memtable_size(),
before + predicted,
"post-insert size must equal size_before + estimate_mutation_size"
);
assert_eq!(engine.memtable_row_count(), 1);
}
#[test]
fn test_deeply_nested_mutation_trips_gate() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_flush_threshold(1024 * 1024)
.with_hard_limit(64 * 1024) .with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
let mut nested = Value::List((0..500).map(|_| Value::text("y".repeat(200))).collect());
for _ in 0..32 {
nested = Value::List(vec![nested]);
}
let table_id = TableId::new("test_ks", "test_table");
let pk = PartitionKey::single("id", Value::Integer(1));
let ops = vec![CellOperation::Write {
column: "name".to_string(),
value: nested,
}];
let mutation = Mutation::new(table_id, pk, None, ops, 1_000_000, None);
let estimate = engine.memtable.estimate_mutation_size(&mutation);
assert!(
estimate >= 500 * 200,
"estimate must reflect ~100KB of real payload, got {estimate}"
);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("hard limit")),
"deeply nested value must trip the hard-limit gate, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
#[test]
fn test_deep_narrow_collection_with_large_scalar_trips_gate() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_flush_threshold(1024 * 1024)
.with_hard_limit(64 * 1024) .with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
let mut nested = Value::List(vec![Value::text("x".repeat(128 * 1024))]);
for _ in 0..32 {
nested = Value::List(vec![nested]);
}
let table_id = TableId::new("test_ks", "test_table");
let pk = PartitionKey::single("id", Value::Integer(1));
let ops = vec![CellOperation::Write {
column: "name".to_string(),
value: nested,
}];
let mutation = Mutation::new(table_id, pk, None, ops, 1_000_000, None);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("hard limit")),
"deep narrow collection with a large scalar must trip the gate, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
fn engine_with_64k_limit() -> (TempDir, WriteEngine) {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_flush_threshold(1024 * 1024)
.with_hard_limit(64 * 1024) .with_durability(Durability::Disabled);
let engine = WriteEngine::new(config).unwrap();
(temp_dir, engine)
}
fn wrap_in_lists(mut inner: Value, levels: usize) -> Value {
for _ in 0..levels {
inner = Value::List(vec![inner]);
}
inner
}
fn mutation_with_value(value: Value) -> Mutation {
let table_id = TableId::new("test_ks", "test_table");
let pk = PartitionKey::single("id", Value::Integer(1));
let ops = vec![CellOperation::Write {
column: "name".to_string(),
value,
}];
Mutation::new(table_id, pk, None, ops, 1_000_000, None)
}
#[test]
fn test_large_scalar_two_levels_deep_rejected() {
let (_tmp, mut engine) = engine_with_64k_limit();
let value = wrap_in_lists(Value::List(vec![Value::text("x".repeat(128 * 1024))]), 1);
let mutation = mutation_with_value(value);
let estimate = engine.memtable.estimate_mutation_size(&mutation);
assert!(
estimate >= 128 * 1024,
"estimate must reflect the buried 128KB scalar, got {estimate}"
);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("hard limit")),
"2-level-buried large scalar must trip the gate, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
#[test]
fn test_large_scalar_three_levels_deep_rejected() {
let (_tmp, mut engine) = engine_with_64k_limit();
let value = wrap_in_lists(Value::List(vec![Value::text("x".repeat(128 * 1024))]), 2);
let mutation = mutation_with_value(value);
let estimate = engine.memtable.estimate_mutation_size(&mutation);
assert!(
estimate >= 128 * 1024,
"estimate must reflect the buried 128KB scalar, got {estimate}"
);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("hard limit")),
"3-level-buried large scalar must trip the gate, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
#[test]
fn test_large_scalar_below_old_depth_cap_rejected() {
let (_tmp, mut engine) = engine_with_64k_limit();
let value = wrap_in_lists(Value::text("x".repeat(128 * 1024)), 33);
let mutation = mutation_with_value(value);
let estimate = engine.memtable.estimate_mutation_size(&mutation);
assert!(
estimate >= 128 * 1024,
"estimate must reflect the 128KB scalar below the old cap, got {estimate}"
);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("hard limit")),
"scalar below old depth cap must trip the gate, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
#[test]
fn test_pathological_node_cap_fails_closed() {
let (_tmp, mut engine) = engine_with_64k_limit();
let value = Value::List((0..1_000_001i32).map(Value::Integer).collect());
let mutation = mutation_with_value(value);
let estimate = engine.memtable.estimate_mutation_size(&mutation);
assert_eq!(
estimate,
usize::MAX,
"hitting the node cap must fail closed with usize::MAX"
);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("fail-closed sentinel")),
"node-cap fail-closed value must be rejected via the sentinel guard, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
#[test]
fn test_sentinel_rejected_with_max_hard_limit() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_flush_threshold(1024 * 1024)
.with_hard_limit(usize::MAX)
.with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
let value = Value::List((0..1_000_001i32).map(Value::Integer).collect());
let mutation = mutation_with_value(value);
assert_eq!(
engine.memtable.estimate_mutation_size(&mutation),
usize::MAX,
"hitting the node cap must fail closed with usize::MAX"
);
let err = engine.write(mutation).unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("fail-closed sentinel")),
"sentinel must be rejected even when hard_limit == usize::MAX, got: {err:?}"
);
assert_eq!(engine.memtable_size(), 0);
}
#[test]
fn test_projected_sum_saturating_no_overflow() {
let temp_dir = TempDir::new().unwrap();
let config = WriteEngineConfig::new(
temp_dir.path().join("data"),
temp_dir.path().join("wal"),
create_test_schema(),
)
.with_durability(Durability::Disabled);
let mut engine = WriteEngine::new(config).unwrap();
engine.memtable.set_size_bytes_for_test(usize::MAX - 8);
let err = engine
.write(create_test_mutation(1, "small", 1_000_000))
.unwrap_err();
assert!(
matches!(err, Error::Storage(ref m) if m.contains("would exceed hard limit")),
"near-usize::MAX size must reject via saturating add, got: {err:?}"
);
}