#![allow(clippy::unwrap_used, clippy::expect_used)]
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
use std::sync::Arc;
fn open(dir: &tempfile::TempDir) -> Arc<Chronix> {
let config = ChronixConfig::builder()
.data_dir(dir.path())
.build()
.unwrap();
Arc::new(Chronix::open(config).unwrap())
}
async fn f64s(ctx: &datafusion::prelude::SessionContext, q: &str) -> Vec<f64> {
let batches = ctx.sql(q).await.unwrap().collect().await.unwrap();
batches
.iter()
.flat_map(|b| {
b.column(0)
.as_any()
.downcast_ref::<arrow::array::Float64Array>()
.unwrap()
.values()
.to_vec()
})
.collect()
}
#[tokio::test]
async fn projection_returns_the_requested_column() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
let key = SeriesKey::new("m", tags! { "h" => "a" }).unwrap();
db.insert(&Point::new(key.clone(), fields! { "power" => 10.0 }, 1000).unwrap())
.unwrap();
db.insert(&Point::new(key.clone(), fields! { "other" => 1.0 }, 2000).unwrap())
.unwrap();
db.flush().unwrap();
let ctx = chronix::sql::create_session_context(db);
assert_eq!(
f64s(&ctx, "SELECT power FROM m WHERE power IS NOT NULL").await,
vec![10.0],
"SELECT power must return power's data"
);
assert_eq!(
f64s(&ctx, "SELECT other FROM m WHERE other IS NOT NULL").await,
vec![1.0],
"SELECT other must return other's data"
);
assert_eq!(f64s(&ctx, "SELECT avg(power) FROM m").await, vec![10.0]);
assert_eq!(f64s(&ctx, "SELECT avg(other) FROM m").await, vec![1.0]);
assert_eq!(f64s(&ctx, "SELECT min(power) FROM m").await, vec![10.0]);
}
#[tokio::test]
async fn column_order_is_independent_of_write_history() {
let names = |ctx: &datafusion::prelude::SessionContext| {
let ctx = ctx.clone();
async move {
ctx.sql("SELECT * FROM m")
.await
.unwrap()
.schema()
.fields()
.iter()
.map(|f| f.name().clone())
.collect::<Vec<_>>()
}
};
let d1 = tempfile::tempdir().unwrap();
let db1 = open(&d1);
let k1 = SeriesKey::new("m", tags! { "h" => "a" }).unwrap();
db1.insert(&Point::new(k1.clone(), fields! { "zeta" => 1.0 }, 1).unwrap())
.unwrap();
db1.insert(&Point::new(k1, fields! { "alpha" => 2.0 }, 2).unwrap())
.unwrap();
db1.flush().unwrap();
let n1 = names(&chronix::sql::create_session_context(db1)).await;
let d2 = tempfile::tempdir().unwrap();
let db2 = open(&d2);
let k2 = SeriesKey::new("m", tags! { "h" => "a" }).unwrap();
db2.insert(&Point::new(k2.clone(), fields! { "alpha" => 2.0 }, 1).unwrap())
.unwrap();
db2.insert(&Point::new(k2, fields! { "zeta" => 1.0 }, 2).unwrap())
.unwrap();
db2.flush().unwrap();
let n2 = names(&chronix::sql::create_session_context(db2)).await;
assert_eq!(n1, n2, "column order must not depend on write order");
assert_eq!(n1, vec!["_time", "h", "alpha", "zeta"]);
}
#[tokio::test]
async fn count_star_over_the_hot_tier_returns_the_row_count() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
let key = SeriesKey::new("m", tags! { "h" => "a" }).unwrap();
for i in 0..5 {
db.insert(&Point::new(key.clone(), fields! { "power" => 1.0 }, 1000 + i).unwrap())
.unwrap();
}
db.flush().unwrap();
let ctx = chronix::sql::create_session_context(db);
let batches = ctx
.sql("SELECT count(*) AS n FROM m")
.await
.unwrap()
.collect()
.await
.unwrap();
let n = batches[0]
.column(0)
.as_any()
.downcast_ref::<arrow::array::Int64Array>()
.unwrap()
.value(0);
assert_eq!(n, 5, "count(*) must see every row");
let batches = ctx
.sql("SELECT count(1) AS n FROM m")
.await
.unwrap()
.collect()
.await
.unwrap();
let n = batches[0]
.column(0)
.as_any()
.downcast_ref::<arrow::array::Int64Array>()
.unwrap()
.value(0);
assert_eq!(n, 5, "count(1) reads no column either");
}
fn pushdown_db(dir: &tempfile::TempDir) -> Chronix {
let db = Chronix::open(
ChronixConfig::builder()
.data_dir(dir.path())
.build()
.unwrap(),
)
.unwrap();
for (i, host) in [(1_i64, "a"), (2, "a"), (3, "b")] {
db.insert(
&Point::new(
SeriesKey::new("m", tags! { "host" => host, "region" => "a" }).unwrap(),
fields! { "v" => i as f64, "n" => i },
i * 1000,
)
.unwrap(),
)
.unwrap();
}
db
}
fn count(db: &Chronix, sql: &str) -> usize {
db.sql(sql)
.unwrap()
.iter()
.map(arrow::array::RecordBatch::num_rows)
.sum()
}
#[test]
fn a_time_inequality_is_actually_applied() {
let dir = tempfile::TempDir::new().unwrap();
let db = pushdown_db(&dir);
assert_eq!(count(&db, "SELECT * FROM m"), 3);
assert_eq!(
count(
&db,
"SELECT * FROM m WHERE _time <> arrow_cast(2000, 'Timestamp(Nanosecond, None)')"
),
2,
"the excluded row came back"
);
assert_eq!(
count(
&db,
"SELECT * FROM m WHERE _time <> arrow_cast(1000, 'Timestamp(Nanosecond, None)') \
ORDER BY _time LIMIT 1"
),
1
);
let batch = &db
.sql(
"SELECT n FROM m WHERE _time <> arrow_cast(1000, 'Timestamp(Nanosecond, None)') \
ORDER BY _time LIMIT 1",
)
.unwrap()[0];
let n = batch
.column_by_name("n")
.unwrap()
.as_any()
.downcast_ref::<arrow::array::Int64Array>()
.unwrap();
assert_eq!(n.value(0), 2, "the limit returned the excluded row");
db.close().unwrap();
}
#[test]
fn a_tag_compared_with_a_column_is_actually_applied() {
let dir = tempfile::TempDir::new().unwrap();
let db = pushdown_db(&dir);
assert_eq!(
count(&db, "SELECT * FROM m WHERE host = region"),
2,
"host = region holds for the two 'a' rows only"
);
assert_eq!(count(&db, "SELECT * FROM m WHERE host <> region"), 1);
db.close().unwrap();
}
#[test]
fn contradictory_time_bounds_return_no_rows() {
let dir = tempfile::TempDir::new().unwrap();
let db = pushdown_db(&dir);
assert_eq!(
count(
&db,
"SELECT * FROM m \
WHERE _time > arrow_cast(3000, 'Timestamp(Nanosecond, None)') \
AND _time < arrow_cast(1000, 'Timestamp(Nanosecond, None)')"
),
0
);
db.close().unwrap();
}
#[test]
fn a_fractional_bound_on_an_integer_field_keeps_its_rows() {
let dir = tempfile::TempDir::new().unwrap();
let db = pushdown_db(&dir);
db.flush().unwrap();
assert_eq!(count(&db, "SELECT * FROM m WHERE n < 1.5"), 1);
assert_eq!(count(&db, "SELECT * FROM m WHERE n > 2.5"), 1);
assert_eq!(count(&db, "SELECT * FROM m WHERE n >= 1.5"), 2);
db.close().unwrap();
}