use cql2::{Expr, ToSqlAst};
fn interval(start: u32, end: u32) -> String {
format!("INTERVAL('2020-01-{start:02}T00:00:00Z','2020-01-{end:02}T00:00:00Z')")
}
fn holds(relation: &str, left: (u32, u32), right: (u32, u32)) -> bool {
let source = format!(
"{relation}({}, {})",
interval(left.0, left.1),
interval(right.0, right.1)
);
let expr: Expr = source
.parse()
.unwrap_or_else(|e| panic!("{source} should parse: {e}"));
match expr.reduce(None) {
Ok(Expr::Bool(value)) => value,
other => panic!("{source} should reduce to a boolean, got {other:?}"),
}
}
fn ranges() -> Vec<(u32, u32)> {
(1..=6)
.flat_map(|start| (start..=6).map(move |end| (start, end)))
.collect()
}
fn pairs() -> Vec<((u32, u32), (u32, u32))> {
let ranges = ranges();
ranges
.iter()
.flat_map(|left| ranges.iter().map(move |right| (*left, *right)))
.collect()
}
type Definition = fn(u32, u32, u32, u32) -> bool;
const DEFINITIONS: [(&str, Definition); 15] = [
("t_before", |_l0, l1, r0, _r1| l1 < r0),
("t_after", |l0, _l1, _r0, r1| r1 < l0),
("t_meets", |_l0, l1, r0, _r1| l1 == r0),
("t_metBy", |l0, _l1, _r0, r1| r1 == l0),
("t_starts", |l0, l1, r0, r1| l0 == r0 && l1 < r1),
("t_startedBy", |l0, l1, r0, r1| l0 == r0 && r1 < l1),
("t_finishes", |l0, l1, r0, r1| r0 < l0 && l1 == r1),
("t_finishedBy", |l0, l1, r0, r1| l0 < r0 && l1 == r1),
("t_during", |l0, l1, r0, r1| r0 < l0 && l1 < r1),
("t_contains", |l0, l1, r0, r1| l0 < r0 && r1 < l1),
("t_equals", |l0, l1, r0, r1| l0 == r0 && l1 == r1),
("t_overlaps", |l0, l1, r0, r1| l0 < r0 && r0 < l1 && l1 < r1),
("t_overlappedBy", |l0, l1, r0, r1| {
r0 < l0 && l0 < r1 && r1 < l1
}),
("t_disjoint", |l0, l1, r0, r1| l1 < r0 || r1 < l0),
("t_intersects", |l0, l1, r0, r1| !(l1 < r0 || r1 < l0)),
];
#[test]
fn relations_match_their_definitions() {
let pairs = pairs();
assert!(
pairs.len() > 400,
"expected a dense pairing, got {}",
pairs.len()
);
let mut failures = Vec::new();
for (relation, defined) in DEFINITIONS {
for (left, right) in &pairs {
let expected = defined(left.0, left.1, right.0, right.1);
let actual = holds(relation, *left, *right);
if actual != expected {
failures.push(format!(
"{relation}([{}-{}],[{}-{}]) = {actual}, definition says {expected}",
left.0, left.1, right.0, right.1
));
}
}
}
assert!(
failures.is_empty(),
"{} results disagree with their definition:\n {}",
failures.len(),
failures.join("\n ")
);
}
#[test]
fn converse_relations_agree() {
const CONVERSES: [(&str, &str); 6] = [
("t_before", "t_after"),
("t_meets", "t_metBy"),
("t_overlaps", "t_overlappedBy"),
("t_starts", "t_startedBy"),
("t_during", "t_contains"),
("t_finishes", "t_finishedBy"),
];
let mut failures = Vec::new();
for (left, right) in pairs() {
for (relation, converse) in CONVERSES {
if holds(relation, left, right) != holds(converse, right, left) {
failures.push(format!(
"{relation}([{}-{}],[{}-{}]) disagrees with its converse {converse}",
left.0, left.1, right.0, right.1
));
}
}
for symmetric in ["t_equals", "t_disjoint", "t_intersects"] {
if holds(symmetric, left, right) != holds(symmetric, right, left) {
failures.push(format!(
"{symmetric} is not symmetric for [{}-{}] and [{}-{}]",
left.0, left.1, right.0, right.1
));
}
}
}
assert!(
failures.is_empty(),
"{} converse mismatches:\n {}",
failures.len(),
failures.join("\n ")
);
}
#[test]
fn disjoint_and_intersects_match_their_definitions() {
let mut failures = Vec::new();
for (left, right) in pairs() {
let disjoint = holds("t_disjoint", left, right);
let expected = holds("t_before", left, right) || holds("t_after", left, right);
if disjoint != expected {
failures.push(format!(
"t_disjoint([{}-{}],[{}-{}]) = {disjoint}, but before-or-after = {expected}",
left.0, left.1, right.0, right.1
));
}
if holds("t_intersects", left, right) == disjoint {
failures.push(format!(
"t_intersects([{}-{}],[{}-{}]) should be the negation of t_disjoint",
left.0, left.1, right.0, right.1
));
}
}
assert!(
failures.is_empty(),
"{} mismatches against the derived definitions:\n {}",
failures.len(),
failures.join("\n ")
);
}
#[test]
fn an_instant_is_a_range_with_coincident_bounds() {
const INSTANT: &str = "TIMESTAMP('2020-01-03T00:00:00Z')";
const DEGENERATE: &str = "INTERVAL('2020-01-03T00:00:00Z','2020-01-03T00:00:00Z')";
let other = interval(1, 5);
let evaluate = |source: &str| -> Option<bool> {
let expr: Expr = source.parse().ok()?;
match expr.reduce(None) {
Ok(Expr::Bool(value)) => Some(value),
_ => None,
}
};
let mut failures = Vec::new();
for (relation, _) in DEFINITIONS {
for as_left in [true, false] {
let (left, right) = if as_left {
(INSTANT, other.as_str())
} else {
(other.as_str(), INSTANT)
};
let (spelled_left, spelled_right) = if as_left {
(DEGENERATE, other.as_str())
} else {
(other.as_str(), DEGENERATE)
};
let as_instant = format!("{relation}({left}, {right})");
let spelled_out = format!("{relation}({spelled_left}, {spelled_right})");
match (evaluate(&as_instant), evaluate(&spelled_out)) {
(Some(a), Some(b)) if a == b => {}
(a, b) => failures.push(format!("{as_instant} = {a:?}, {spelled_out} = {b:?}")),
}
}
}
assert!(
failures.is_empty(),
"{} instants did not behave as their degenerate range:\n {}",
failures.len(),
failures.join("\n ")
);
}
#[test]
fn non_temporal_operands_are_rejected() {
for source in [
"t_meets(1, 2)",
"t_during(POINT(0 0), INTERVAL('2020-01-01T00:00:00Z','2020-01-05T00:00:00Z'))",
] {
let expr: Expr = source.parse().expect("expression parses");
assert!(
expr.to_sql_ast().is_err(),
"{source} should not render as SQL"
);
}
}