mod support {
pub mod parser_hardening;
}
use proptest::prelude::*;
use reddb_server::storage::query::parser::{self, ParseError, ParserLimits};
use support::parser_hardening::{
self as harness, assert_no_panic_on, corpus::timeseries_adversarial_inputs, timeseries_grammar,
HardenedParser,
};
pub struct TimeseriesParser;
impl HardenedParser for TimeseriesParser {
type Error = ParseError;
fn parse(input: &str) -> Result<(), Self::Error> {
parser::parse(input).map(|_| ())
}
fn parse_with_limits(input: &str, limits: ParserLimits) -> Result<(), Self::Error> {
let mut p = parser::Parser::with_limits(input, limits)?;
p.parse().map(|_| ())
}
}
#[test]
fn timeseries_parser_does_not_panic_on_adversarial_corpus() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
for (name, input) in timeseries_adversarial_inputs() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_no_panic_on::<TimeseriesParser>(&input);
}));
if result.is_err() {
panic!("timeseries adversarial corpus entry {} panicked", name);
}
}
})
.expect("spawn corpus thread");
handle.join().expect("corpus thread panic");
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn proptest_create_timeseries_roundtrips(s in timeseries_grammar::create_timeseries_stmt()) {
harness::roundtrip_property::<TimeseriesParser>(&s);
prop_assert!(
TimeseriesParser::parse(&s).is_ok(),
"create timeseries did not parse: {}", s
);
}
#[test]
fn proptest_create_hypertable_roundtrips(s in timeseries_grammar::create_hypertable_stmt()) {
harness::roundtrip_property::<TimeseriesParser>(&s);
prop_assert!(
TimeseriesParser::parse(&s).is_ok(),
"create hypertable did not parse: {}", s
);
}
#[test]
fn proptest_chunk_interval_units_roundtrip(
s in timeseries_grammar::chunk_interval_focused_stmt()
) {
harness::roundtrip_property::<TimeseriesParser>(&s);
prop_assert!(
TimeseriesParser::parse(&s).is_ok(),
"chunk interval focused stmt did not parse: {}", s
);
}
#[test]
fn proptest_retention_clause_roundtrip(
s in timeseries_grammar::retention_focused_stmt()
) {
harness::roundtrip_property::<TimeseriesParser>(&s);
prop_assert!(
TimeseriesParser::parse(&s).is_ok(),
"retention focused stmt did not parse: {}", s
);
}
#[test]
fn proptest_continuous_aggregate_roundtrips(
s in timeseries_grammar::continuous_aggregate_stmt()
) {
harness::roundtrip_property::<TimeseriesParser>(&s);
prop_assert!(
TimeseriesParser::parse(&s).is_ok(),
"continuous aggregate did not parse: {}", s
);
}
#[test]
fn proptest_timeseries_arbitrary_suffix_no_panic(
prefix in prop_oneof![
Just("CREATE TIMESERIES ".to_string()),
Just("CREATE HYPERTABLE ".to_string()),
Just("DROP TIMESERIES ".to_string()),
Just("CREATE MATERIALIZED VIEW ".to_string()),
],
suffix in ".{0,512}",
) {
let s = format!("{}{}", prefix, suffix);
harness::roundtrip_property::<TimeseriesParser>(&s);
}
#[test]
fn proptest_timeseries_input_size_limit_enforced(len in 200usize..2000) {
let limits = ParserLimits {
max_input_bytes: 64,
..ParserLimits::default()
};
let body = "x".repeat(len);
let input = format!("CREATE TIMESERIES m1 {}", body);
let r = TimeseriesParser::parse_with_limits(&input, limits);
prop_assert!(r.is_err(), "oversized timeseries body must error");
}
}
use reddb_server::storage::query::ast::QueryExpr;
fn parse_query(input: &str) -> QueryExpr {
parser::parse(input)
.unwrap_or_else(|e| panic!("expected ok for {input:?}, got error: {e}"))
.query
}
#[test]
fn create_timeseries_with_retention_days_parses() {
let q = parse_query("CREATE TIMESERIES cpu_metrics RETENTION 90 d");
match q {
QueryExpr::CreateTimeSeries(ts) => {
assert_eq!(ts.name, "cpu_metrics");
assert_eq!(ts.retention_ms, Some(90 * 86_400_000));
assert!(ts.hypertable.is_none());
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}
#[test]
fn create_timeseries_with_downsample_policies_parses() {
let q =
parse_query("CREATE TIMESERIES cpu_metrics RETENTION 90 d DOWNSAMPLE 1h:5m:avg, 1d:1h:max");
match q {
QueryExpr::CreateTimeSeries(ts) => {
assert_eq!(
ts.downsample_policies,
vec!["1h:5m:avg".to_string(), "1d:1h:max".to_string()]
);
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}
#[test]
fn create_timeseries_with_chunk_size_parses() {
let q = parse_query("CREATE TIMESERIES m1 CHUNK_SIZE 4096");
match q {
QueryExpr::CreateTimeSeries(ts) => {
assert_eq!(ts.name, "m1");
assert_eq!(ts.chunk_size, Some(4096));
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}
#[test]
fn create_hypertable_minimal_parses_with_required_clauses() {
let q = parse_query("CREATE HYPERTABLE metrics TIME_COLUMN ts CHUNK_INTERVAL '1d'");
match q {
QueryExpr::CreateTimeSeries(ts) => {
let ht = ts.hypertable.expect("hypertable spec populated");
assert_eq!(ts.name, "metrics");
assert_eq!(ht.time_column, "ts");
assert_eq!(ht.chunk_interval_ns, 86_400_000_000_000);
assert!(ht.default_ttl_ns.is_none());
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}
#[test]
fn create_hypertable_with_ttl_and_retention_parses() {
let q = parse_query(
"CREATE HYPERTABLE metrics TIME_COLUMN ts CHUNK_INTERVAL '1d' TTL '90d' RETENTION 90 d",
);
match q {
QueryExpr::CreateTimeSeries(ts) => {
let ht = ts.hypertable.expect("hypertable spec populated");
assert_eq!(ht.chunk_interval_ns, 86_400_000_000_000);
assert_eq!(ht.default_ttl_ns, Some(90 * 86_400_000_000_000));
assert_eq!(ts.retention_ms, Some(90 * 86_400_000));
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}
#[test]
fn create_hypertable_clause_order_is_unrestricted() {
let q = parse_query(
"CREATE HYPERTABLE metrics RETENTION 30 d TIME_COLUMN ts TTL '7d' CHUNK_INTERVAL '1h'",
);
match q {
QueryExpr::CreateTimeSeries(ts) => {
let ht = ts.hypertable.expect("hypertable spec populated");
assert_eq!(ht.time_column, "ts");
assert_eq!(ht.chunk_interval_ns, 3_600_000_000_000);
assert_eq!(ht.default_ttl_ns, Some(7 * 86_400_000_000_000));
assert_eq!(ts.retention_ms, Some(30 * 86_400_000));
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}
#[test]
fn drop_timeseries_with_if_exists_parses() {
let q = parse_query("DROP TIMESERIES IF EXISTS m1");
match q {
QueryExpr::DropTimeSeries(ts) => {
assert_eq!(ts.name, "m1");
assert!(ts.if_exists);
}
other => panic!("expected DropTimeSeries, got {other:?}"),
}
}
#[test]
fn create_materialized_view_for_continuous_aggregate_parses() {
let q =
parse_query("CREATE MATERIALIZED VIEW IF NOT EXISTS cpu_5m AS SELECT id FROM cpu_metrics");
match q {
QueryExpr::CreateView(_) => {}
other => panic!("expected CreateView, got {other:?}"),
}
}
#[test]
fn refresh_materialized_view_parses() {
let q = parse_query("REFRESH MATERIALIZED VIEW cpu_5m");
match q {
QueryExpr::RefreshMaterializedView(_) => {}
other => panic!("expected RefreshMaterializedView, got {other:?}"),
}
}
#[test]
fn create_hypertable_with_hour_chunk_interval_parses() {
let q = parse_query("CREATE HYPERTABLE metrics TIME_COLUMN ts CHUNK_INTERVAL '6h'");
match q {
QueryExpr::CreateTimeSeries(ts) => {
let ht = ts.hypertable.expect("hypertable spec populated");
assert_eq!(ht.chunk_interval_ns, 6 * 3_600_000_000_000);
}
other => panic!("expected CreateTimeSeries, got {other:?}"),
}
}