#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
use std::sync::Arc;
fn fixture(dir: &tempfile::TempDir) -> Arc<Chronix> {
let config = ChronixConfig::builder()
.data_dir(dir.path())
.build()
.unwrap();
let db = Arc::new(Chronix::open(config).unwrap());
db.declare_field(
"meter",
"reading",
chronix_core::ColumnType::Decimal { scale: 4 },
)
.unwrap();
let now = 1_700_000_000_000_000_000i64;
let insert = |measurement: &str, tags, fields, ts: i64| {
db.insert(&Point::new(SeriesKey::new(measurement, tags).unwrap(), fields, ts).unwrap())
.unwrap();
};
for i in 0..3i64 {
let ts = now + i * 60_000_000_000;
insert(
"cpu",
tags! { "host" => "web-01" },
fields! { "usage" => 1.0, "usage_idle" => 99.0, "value" => 1.0 },
ts,
);
insert(
"cpu_usage",
tags! { "host" => "web-01" },
fields! { "usage" => 1.0, "value" => 1.0 },
ts,
);
insert(
"metrics",
tags! { "metric_name" => "cpu" },
fields! { "value" => 1.0 },
ts,
);
insert(
"temp",
tags! { "site" => "a" },
fields! { "value" => 1.0 },
ts,
);
insert(
"meter",
tags! { "device" => "main" },
fields! { "reading" => "1234.5678".parse::<chronix_core::Decimal>().unwrap() },
ts,
);
}
db
}
fn documented_blocks() -> Vec<(String, String)> {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("repo root");
let mut blocks = Vec::new();
let mut stack = vec![root.join("site").join("content")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("readable docs directory") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().is_none_or(|e| e != "md") {
continue;
}
let text = std::fs::read_to_string(&path).expect("readable markdown");
let name = path
.strip_prefix(&root)
.unwrap_or(&path)
.display()
.to_string();
for (i, block) in fenced_sql(&text).into_iter().enumerate() {
blocks.push((format!("{name} [{i}]"), block));
}
}
}
blocks.sort();
blocks
}
fn fenced_sql(text: &str) -> Vec<String> {
let mut blocks = Vec::new();
let mut current: Option<String> = None;
for line in text.lines() {
match current {
None if line.trim_start().starts_with("```sql") => current = Some(String::new()),
None => {}
Some(_) if line.trim_start() == "```" => {
blocks.push(current.take().unwrap_or_default());
}
Some(ref mut body) => {
body.push_str(line);
body.push('\n');
}
}
}
blocks
}
fn statements(block: &str) -> Vec<String> {
let without_comments: String = block
.lines()
.filter(|l| !l.trim_start().starts_with("--"))
.collect::<Vec<_>>()
.join("\n");
let mut statements = Vec::new();
let mut current = String::new();
let mut in_string = false;
for c in without_comments.chars() {
match c {
'\'' => {
in_string = !in_string;
current.push(c);
}
';' if !in_string => {
statements.push(std::mem::take(&mut current));
}
_ => current.push(c),
}
}
statements.push(current);
statements
.into_iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn is_trigger_dsl(sql: &str) -> bool {
let head = sql.trim_start().to_ascii_uppercase();
head.starts_with("CREATE TRIGGER")
|| head.starts_with("DROP TRIGGER")
|| head.starts_with("ALTER TRIGGER")
|| head.starts_with("SHOW TRIGGERS")
}
#[test]
fn every_documented_sql_statement_plans() {
let dir = tempfile::tempdir().unwrap();
let db = fixture(&dir);
let ctx = chronix::sql::create_session_context(db.clone());
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let blocks = documented_blocks();
assert!(
blocks.len() >= 10,
"the extractor found only {} blocks, so it is not working",
blocks.len()
);
let mut failures: Vec<String> = Vec::new();
let mut checked = 0usize;
for (origin, block) in blocks {
for sql in statements(&block) {
checked += 1;
let result = if is_trigger_dsl(&sql) {
chronix::chronix_streaming::signal::parse_trigger_sql(&sql)
.map(|_| ())
.map_err(|e| e.to_string())
} else {
rt.block_on(chronix::sql::plan_read_only(&ctx, &sql))
.map(|_| ())
.map_err(|e| e.to_string())
};
if let Err(e) = result {
let reason = e.lines().last().unwrap_or(&e).trim().to_string();
failures.push(format!(
"{origin}\n {}\n -> {reason}",
sql.replace('\n', "\n ")
));
}
}
}
assert!(
checked >= 15,
"only {checked} statements were checked, so the splitter is not working"
);
assert!(
failures.is_empty(),
"documented SQL that does not plan — either the example is wrong or \
the fixture must grow with it:\n\n{}",
failures.join("\n\n")
);
}