#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use arrow::array::Array;
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
use std::sync::Arc;
const BASE: i64 = 1_700_000_000_000_000_000;
const SECOND: i64 = 1_000_000_000;
fn open(dir: &tempfile::TempDir) -> Arc<Chronix> {
let config = ChronixConfig::builder()
.data_dir(dir.path())
.build()
.unwrap();
Arc::new(Chronix::open(config).unwrap())
}
fn point(host: &str, ts: i64, v: f64) -> Point {
Point::new(
SeriesKey::new("m", tags! { "host" => host }).unwrap(),
fields! { "v" => v },
ts,
)
.unwrap()
}
fn seeded(dir: &tempfile::TempDir) -> Arc<Chronix> {
let db = open(dir);
for i in 0..5i64 {
db.insert(&point("a", BASE + i * SECOND, i as f64)).unwrap();
}
db.flush().unwrap();
db
}
fn rows(db: &Chronix, sql: &str) -> usize {
db.sql(sql)
.unwrap_or_else(|e| panic!("{sql}\n failed: {e}"))
.iter()
.map(arrow::array::RecordBatch::num_rows)
.sum()
}
fn scan_line(db: &Chronix, sql: &str) -> String {
let batches = db
.sql(&format!("EXPLAIN {sql}"))
.unwrap_or_else(|e| panic!("EXPLAIN {sql}\n failed: {e}"));
for b in &batches {
for c in 0..b.num_columns() {
let Some(a) = b
.column(c)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
else {
continue;
};
for i in 0..a.len() {
for line in a.value(i).lines() {
if line.contains("ChronixExec") {
return line.trim().to_string();
}
}
}
}
}
panic!("no ChronixExec in the plan for: {sql}");
}
#[test]
fn a_time_column_compares_against_an_epoch_integer() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
let t2 = BASE + 2 * SECOND;
assert_eq!(
rows(&db, &format!("SELECT * FROM m WHERE _time >= {BASE}")),
5
);
assert_eq!(
rows(&db, &format!("SELECT * FROM m WHERE _time <= {t2}")),
3
);
assert_eq!(rows(&db, &format!("SELECT * FROM m WHERE _time > {t2}")), 2);
assert_eq!(rows(&db, &format!("SELECT * FROM m WHERE _time < {t2}")), 2);
assert_eq!(rows(&db, &format!("SELECT * FROM m WHERE _time = {t2}")), 1);
assert_eq!(
rows(&db, &format!("SELECT * FROM m WHERE _time <> {t2}")),
4
);
}
#[test]
fn between_and_in_accept_epoch_integers() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
let t0 = BASE;
let t2 = BASE + 2 * SECOND;
assert_eq!(
rows(
&db,
&format!("SELECT * FROM m WHERE _time BETWEEN {t0} AND {t2}")
),
3
);
assert_eq!(
rows(
&db,
&format!("SELECT * FROM m WHERE _time NOT BETWEEN {t0} AND {t2}")
),
2
);
assert_eq!(
rows(&db, &format!("SELECT * FROM m WHERE _time IN ({t0}, {t2})")),
2
);
}
#[test]
fn an_epoch_literal_is_read_in_the_columns_unit() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
let t2 = BASE + 2 * SECOND;
let batches = db
.sql(&format!("SELECT v FROM m WHERE _time = {t2}"))
.unwrap();
let values: Vec<f64> = batches
.iter()
.flat_map(|b| {
let a = b
.column(0)
.as_any()
.downcast_ref::<arrow::array::Float64Array>()
.unwrap();
(0..a.len()).map(|i| a.value(i)).collect::<Vec<_>>()
})
.collect();
assert_eq!(values, vec![2.0], "the literal must name the third point");
}
#[test]
fn the_literal_may_be_on_either_side() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
let t2 = BASE + 2 * SECOND;
assert_eq!(
rows(&db, &format!("SELECT * FROM m WHERE {t2} <= _time")),
3
);
}
#[test]
fn a_float_literal_is_left_alone() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
assert!(
db.sql("SELECT * FROM m WHERE _time >= 1.7e18").is_err(),
"a float must not be silently read as an instant"
);
}
#[test]
fn an_epoch_predicate_narrows_the_scan() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
let t2 = BASE + 2 * SECOND;
let unfiltered = scan_line(&db, "SELECT * FROM m");
assert!(
unfiltered.contains(&i64::MIN.to_string()),
"an unfiltered scan reads everything: {unfiltered}"
);
for sql in [
format!("SELECT * FROM m WHERE _time >= {BASE}"),
"SELECT * FROM m WHERE _time >= timestamp '2023-11-14T22:13:20Z'".to_string(),
"SELECT * FROM m WHERE _time >= to_timestamp_seconds(1700000000)".to_string(),
"SELECT * FROM m WHERE _time >= to_timestamp_millis(1700000000000)".to_string(),
] {
let line = scan_line(&db, &sql);
assert!(
line.contains(&format!("time=[{BASE}..")),
"the lower bound must reach the scan: {sql}\n {line}"
);
}
let line = scan_line(
&db,
&format!("SELECT * FROM m WHERE _time BETWEEN {BASE} AND {t2}"),
);
assert!(
line.contains(&format!("time=[{BASE}..{t2}]")),
"BETWEEN must narrow both ends: {line}"
);
}
#[test]
fn explain_is_permitted_on_a_read() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
assert!(db.sql("EXPLAIN SELECT * FROM m").is_ok());
assert!(db.sql("EXPLAIN ANALYZE SELECT count(*) FROM m").is_ok());
assert!(
db.sql("EXPLAIN SELECT * FROM m WHERE v > (SELECT avg(v) FROM m)")
.is_ok(),
"a subquery is still a read"
);
}
#[test]
fn explain_does_not_smuggle_a_write_past_the_check() {
let dir = tempfile::tempdir().unwrap();
let db = seeded(&dir);
for sql in [
"EXPLAIN INSERT INTO m SELECT * FROM m",
"EXPLAIN ANALYZE INSERT INTO m SELECT * FROM m",
"EXPLAIN SET datafusion.catalog.information_schema = true",
"EXPLAIN CREATE EXTERNAL TABLE t STORED AS CSV LOCATION '/etc/passwd'",
] {
assert!(
db.sql(sql).is_err(),
"{sql} must not be permitted by wrapping it in EXPLAIN"
);
}
}