#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
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())
}
fn seed(db: &Chronix, scale: u8, values: &[&str]) {
db.declare_field("meter", "v", ColumnType::Decimal { scale })
.unwrap();
let key = SeriesKey::new("meter", tags! { "dev" => "main" }).unwrap();
for (i, text) in values.iter().enumerate() {
db.insert(
&Point::new(
key.clone(),
fields! { "v" => text.parse::<Decimal>().unwrap() },
1_000 + i as i64,
)
.unwrap(),
)
.unwrap();
}
db.flush().unwrap();
}
fn count(db: &Chronix, sql: &str) -> i64 {
let batches = db.sql(sql).unwrap();
batches[0]
.column(0)
.as_any()
.downcast_ref::<arrow::array::Int64Array>()
.unwrap_or_else(|| panic!("count is not Int64 for: {sql}"))
.value(0)
}
fn one_decimal(db: &Chronix, sql: &str) -> String {
let batches = db.sql(sql).unwrap();
let col = batches[0].column(0);
let arr = col
.as_any()
.downcast_ref::<arrow::array::Decimal128Array>()
.unwrap_or_else(|| {
panic!(
"expected a decimal result for `{sql}`, got {:?} — the exactness \
was lost somewhere in planning",
col.data_type()
)
});
let scale = u8::try_from(arr.scale()).unwrap();
Decimal::new(arr.value(0), scale).unwrap().to_string()
}
#[test]
fn a_literal_comparison_does_not_go_through_a_double() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 20, &["0.10000000000000000001"]);
assert_eq!(
count(&db, "SELECT count(*) FROM meter WHERE v > 0.1"),
1,
"a value one ulp above the literal must be greater than it"
);
assert_eq!(
count(&db, "SELECT count(*) FROM meter WHERE v = 0.1"),
0,
"and must not be equal to it"
);
}
#[test]
fn a_between_range_is_exact_at_its_endpoints() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 2, &["1.14", "1.15", "1.16"]);
assert_eq!(
count(
&db,
"SELECT count(*) FROM meter WHERE v BETWEEN 1.15 AND 1.16"
),
2
);
assert_eq!(count(&db, "SELECT count(*) FROM meter WHERE v > 1.15"), 1);
assert_eq!(count(&db, "SELECT count(*) FROM meter WHERE v >= 1.15"), 2);
}
#[test]
fn an_in_list_matches_exactly() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 4, &["0.1000", "0.2000", "0.3000"]);
assert_eq!(
count(&db, "SELECT count(*) FROM meter WHERE v IN (0.1, 0.3)"),
2
);
}
#[test]
fn sum_is_exact_where_a_double_would_drift() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 2, &["0.10"; 10]);
assert_eq!(one_decimal(&db, "SELECT sum(v) FROM meter"), "1.00");
}
#[test]
fn min_and_max_stay_decimals() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 4, &["1.1111", "3.3333", "2.2222"]);
assert_eq!(one_decimal(&db, "SELECT min(v) FROM meter"), "1.1111");
assert_eq!(one_decimal(&db, "SELECT max(v) FROM meter"), "3.3333");
}
#[test]
fn arithmetic_on_a_decimal_column_stays_decimal() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 2, &["1.15"]);
assert_eq!(one_decimal(&db, "SELECT v * 3 FROM meter"), "3.45");
assert_eq!(one_decimal(&db, "SELECT v + 0.05 FROM meter"), "1.20");
assert_eq!(one_decimal(&db, "SELECT v - 0.15 FROM meter"), "1.00");
}
#[test]
fn a_difference_of_registers_is_the_consumption() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 4, &["1000.0001", "1000.2001"]);
assert_eq!(
one_decimal(&db, "SELECT max(v) - min(v) FROM meter"),
"0.2000"
);
}
#[test]
fn grouping_by_a_decimal_column_groups_on_the_value_not_a_float() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(
&db,
20,
&["0.10000000000000000001", "0.10000000000000000002"],
);
assert_eq!(
count(&db, "SELECT count(*) FROM (SELECT v FROM meter GROUP BY v)"),
2,
"two distinct decimals must not collapse into one group"
);
}
#[test]
fn casting_to_double_is_available_and_is_the_only_lossy_step() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 2, &["1.15"]);
let batches = db
.sql("SELECT CAST(v AS DOUBLE) AS approx FROM meter")
.unwrap();
let col = batches[0]
.column(0)
.as_any()
.downcast_ref::<arrow::array::Float64Array>()
.expect("an explicit cast produces a float, and says so in the schema");
assert!((col.value(0) - 1.15).abs() < 1e-12);
}
#[test]
fn the_column_type_is_visible_to_a_client() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 4, &["1.0000"]);
let batches = db.sql("SELECT v FROM meter LIMIT 1").unwrap();
assert_eq!(
batches[0].schema().field(0).data_type(),
&arrow::datatypes::DataType::Decimal128(38, 4)
);
}
#[test]
fn a_float_column_is_untouched_by_the_literal_change() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
let key = SeriesKey::new("cpu", tags! { "h" => "a" }).unwrap();
db.insert(&Point::new(key, fields! { "usage" => 2.0_f64 }, 1).unwrap())
.unwrap();
db.flush().unwrap();
for sql in [
"SELECT usage * 1.5 FROM cpu",
"SELECT usage + 1.5 FROM cpu",
"SELECT avg(usage) FROM cpu",
] {
let batches = db.sql(sql).unwrap();
assert_eq!(
batches[0].schema().field(0).data_type(),
&arrow::datatypes::DataType::Float64,
"`{sql}` must still produce a float"
);
}
assert_eq!(count(&db, "SELECT count(*) FROM cpu WHERE usage > 1.5"), 1);
}
#[test]
fn an_explicit_cast_is_the_way_to_force_a_float_literal() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
seed(&db, 2, &["1.15"]);
assert_eq!(
db.sql("SELECT v + 1.5e0 FROM meter").unwrap()[0]
.schema()
.field(0)
.data_type(),
&arrow::datatypes::DataType::Decimal128(38, 2),
"the exponent form is a decimal too"
);
assert_eq!(
db.sql("SELECT v + CAST(1.5 AS DOUBLE) FROM meter").unwrap()[0]
.schema()
.field(0)
.data_type(),
&arrow::datatypes::DataType::Float64,
"an explicit cast is the escape hatch"
);
}
#[test]
fn a_quarter_hour_report_sums_exactly_per_bucket() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir);
db.declare_field("meter", "v", ColumnType::Decimal { scale: 4 })
.unwrap();
let key = SeriesKey::new("meter", tags! { "dev" => "main" }).unwrap();
let hour = 3_600_000_000_000_i64;
for bucket in 0..2_i64 {
for i in 0..5_i64 {
db.insert(
&Point::new(
key.clone(),
fields! { "v" => "0.0100".parse::<Decimal>().unwrap() },
bucket * hour + i * 60_000_000_000,
)
.unwrap(),
)
.unwrap();
}
}
db.flush().unwrap();
let batches = db
.sql(
"SELECT time_bucket('1h', _time) AS b, sum(v) AS total \
FROM meter GROUP BY b ORDER BY b",
)
.unwrap();
let batch = &batches[0];
let totals = batch
.column_by_name("total")
.unwrap()
.as_any()
.downcast_ref::<arrow::array::Decimal128Array>()
.expect("a per-bucket total of a decimal column is a decimal");
let scale = u8::try_from(totals.scale()).unwrap();
assert_eq!(batch.num_rows(), 2);
for row in 0..2 {
assert_eq!(
Decimal::new(totals.value(row), scale).unwrap().to_string(),
"0.0500"
);
}
}