mod test_helpers;
use serde_json::json;
use test_helpers::TestDb;
fn scalar(q: &str) -> serde_json::Value {
TestDb::new().scalar(q)
}
#[test]
fn zero_argument_constructors_return_their_own_type() {
for (call, ty) in [
("date()", "DATE"),
("datetime()", "DATETIME"),
("localdatetime()", "LOCAL_DATETIME"),
("time()", "TIME"),
("localtime()", "LOCAL_TIME"),
] {
assert_eq!(
scalar(&format!("RETURN type.of({call})")),
json!(ty),
"{call}"
);
}
assert_eq!(scalar("RETURN date() = temporal.today()"), json!(true));
assert_eq!(scalar("RETURN type.of(DATE())"), json!("DATE"));
}
#[test]
fn date_compares_with_dates_as_the_brief_expects() {
assert_eq!(
scalar("RETURN date() + duration('P1D') >= date()"),
json!(true)
);
assert_eq!(scalar("RETURN date('1999-01-01') >= date()"), json!(false));
assert_eq!(
scalar("RETURN date(datetime()) = date()"),
json!(true),
"date(datetime()) is today's date"
);
assert_eq!(
scalar("RETURN date(substring(toString(datetime()), 0, 10)) = date()"),
json!(true)
);
}
#[test]
fn string_arguments_parse_as_the_named_type() {
for (q, want) in [
("RETURN toString(date('2026-10-01'))", "2026-10-01"),
(
"RETURN toString(date('2026-10-01T23:30:00+02:00'))",
"2026-10-01",
),
(
"RETURN toString(datetime('2026-10-01'))",
"2026-10-01T00:00:00Z",
),
(
"RETURN toString(datetime('2026-10-01T12:00:00+02:00'))",
"2026-10-01T12:00:00+02:00",
),
(
"RETURN toString(localdatetime('2026-10-01T12:00'))",
"2026-10-01T12:00:00",
),
(
"RETURN toString(localdatetime('2026-10-01'))",
"2026-10-01T00:00:00",
),
("RETURN toString(time('12:00Z'))", "12:00:00Z"),
("RETURN toString(time('12:00+01:00'))", "12:00:00+01:00"),
("RETURN toString(localtime('12:00'))", "12:00:00"),
("RETURN toString(duration('P1DT2H'))", "P1DT2H"),
] {
assert_eq!(scalar(q), json!(want), "{q}");
}
}
#[test]
fn temporal_arguments_keep_the_components_the_target_has() {
for (q, want) in [
(
"RETURN toString(date(datetime('2026-10-01T23:30:00+02:00')))",
"2026-10-01",
),
(
"RETURN toString(date(localdatetime('2026-10-01T23:30')))",
"2026-10-01",
),
(
"RETURN toString(datetime(date('2026-10-01')))",
"2026-10-01T00:00:00Z",
),
(
"RETURN toString(datetime(localdatetime('2026-10-01T08:00')))",
"2026-10-01T08:00:00Z",
),
(
"RETURN toString(localdatetime(datetime('2026-10-01T08:00:00+02:00')))",
"2026-10-01T08:00:00",
),
(
"RETURN toString(localdatetime(date('2026-10-01')))",
"2026-10-01T00:00:00",
),
(
"RETURN toString(time(datetime('2026-10-01T08:00:00+02:00')))",
"08:00:00+02:00",
),
("RETURN toString(time(localtime('08:00')))", "08:00:00Z"),
(
"RETURN toString(localtime(datetime('2026-10-01T08:00:00+02:00')))",
"08:00:00",
),
(
"RETURN toString(localtime(time('08:00+02:00')))",
"08:00:00",
),
] {
assert_eq!(scalar(q), json!(want), "{q}");
}
}
#[test]
fn map_arguments_give_the_components() {
for (q, want) in [
("RETURN toString(date({year: 2026, month: 2, day: 3}))", "2026-02-03"),
("RETURN toString(date({year: 2026}))", "2026-01-01"),
(
"RETURN toString(datetime({year: 2026, month: 2, day: 3, hour: 4, minute: 5, timezone: '+01:00'}))",
"2026-02-03T04:05:00+01:00",
),
(
"RETURN toString(localdatetime({year: 2026, month: 2, day: 3, hour: 4}))",
"2026-02-03T04:00:00",
),
(
"RETURN toString(time({hour: 4, minute: 5, timezone: 'Z'}))",
"04:05:00Z",
),
(
"RETURN toString(localtime({hour: 4, minute: 5, second: 6, millisecond: 7}))",
"04:05:06.007",
),
("RETURN toString(duration({days: 2, hours: 3}))", "P2DT3H"),
] {
assert_eq!(scalar(q), json!(want), "{q}");
}
}
#[test]
fn bad_arguments_fail_instead_of_returning_null_or_now() {
let db = TestDb::new();
for q in [
"RETURN date('not a date')",
"RETURN time('25:00')",
"RETURN date({month: 2, day: 3})",
"RETURN date(time('12:00Z'))",
"RETURN localtime(date('2026-10-01'))",
"RETURN datetime({year: 2026, timezone: 'Mars/Olympus'})",
"RETURN date(42)",
] {
let err = db.run_err(q);
assert!(err.contains("cannot cast"), "{q}: {err}");
}
for q in ["RETURN date('2026-01-01', 'x')", "RETURN duration()"] {
let err = db.run_err(q);
assert!(err.contains("argument"), "{q}: {err}");
}
assert_eq!(db.scalar("RETURN date(null)"), json!(null));
assert_eq!(db.scalar("RETURN datetime(null)"), json!(null));
}
fn festivals(indexed: bool) -> TestDb {
let db = TestDb::new();
if indexed {
db.run("CREATE INDEX festival_starts FOR (f:Festival) ON (f.startsOn)");
}
db.run(
"CREATE (:Festival {key: 'past', startsOn: date() - duration('P10D')}), \
(:Festival {key: 'today', startsOn: date()}), \
(:Festival {key: 'soon', startsOn: date() + duration('P10D')})",
);
db
}
#[test]
fn where_on_date_keeps_the_matching_rows_on_both_plans() {
for indexed in [false, true] {
let db = festivals(indexed);
assert_eq!(
db.sorted_strings(
"MATCH (f:Festival) WHERE f.startsOn >= date() RETURN f.key AS k",
"k"
),
vec!["soon", "today"],
"indexed: {indexed}"
);
assert_eq!(
db.sorted_strings(
"MATCH (f:Festival) WHERE f.startsOn >= date() RETURN f.key AS k \
ORDER BY f.startsOn LIMIT 1",
"k"
),
vec!["today"],
"indexed: {indexed}"
);
}
}
#[test]
fn comparing_a_date_with_a_datetime_fails_on_every_plan() {
for indexed in [false, true] {
let db = festivals(indexed);
for q in [
"MATCH (f:Festival) WHERE f.startsOn >= datetime() RETURN f.key AS k",
"MATCH (f:Festival) WHERE f.startsOn < datetime() RETURN f.key AS k",
"MATCH (f:Festival) WHERE f.startsOn >= datetime() RETURN f.key AS k \
ORDER BY f.startsOn LIMIT 1",
"MATCH (f:Festival) WHERE f.startsOn > date() AND f.startsOn < datetime() \
RETURN f.key AS k",
"MATCH (f:Festival {key: 'soon'}) WITH f MATCH (f) \
WHERE f.startsOn >= datetime() RETURN f.key AS k",
] {
let err = db.run_err(q);
assert!(
err.contains("cannot compare") && err.contains("DATETIME"),
"indexed: {indexed}, {q}: {err}"
);
}
}
}
#[test]
fn comparing_relationship_temporals_of_different_kinds_fails_on_every_plan() {
for indexed in [false, true] {
let db = TestDb::new();
if indexed {
db.run("CREATE INDEX hired_on FOR ()-[r:HIRED]-() ON (r.on)");
}
db.run("CREATE (:P)-[:HIRED {on: date('2020-03-15')}]->(:C)");
assert_eq!(
db.run("MATCH ()-[r:HIRED]->() WHERE r.on < date() RETURN count(r) AS n"),
vec![json!({ "n": 1 })]
);
let err = db.run_err("MATCH ()-[r:HIRED]->() WHERE r.on < datetime() RETURN r");
assert!(err.contains("cannot compare"), "indexed: {indexed}: {err}");
}
}
#[test]
fn every_pair_of_different_temporal_kinds_fails_to_order() {
let values = [
"date('2026-10-01')",
"datetime('2026-10-01T00:00Z')",
"localdatetime('2026-10-01T00:00')",
"time('00:00Z')",
"localtime('00:00')",
];
let db = TestDb::new();
for (i, a) in values.iter().enumerate() {
for (j, b) in values.iter().enumerate() {
for op in ["<", "<=", ">", ">="] {
let q = format!("RETURN {a} {op} {b} AS r");
if i == j {
assert!(db.scalar(&q).is_boolean(), "{q}");
} else {
let err = db.run_err(&q);
assert!(err.contains("cannot compare"), "{q}: {err}");
}
}
let eq = db.scalar(&format!("RETURN {a} = {b}"));
assert_eq!(eq, json!(i == j), "{a} = {b}");
}
}
assert_eq!(
db.scalar("RETURN date('2026-10-01') < '2026-10-02'"),
json!(null)
);
assert_eq!(db.scalar("RETURN date('2026-10-01') < 1"), json!(null));
}
#[test]
fn sorting_and_aggregating_mixed_kinds_still_works() {
let db = TestDb::new();
db.run("CREATE (:E {t: date('2026-10-01')}), (:E {t: datetime('2026-10-01T00:00Z')})");
assert_eq!(db.run("MATCH (e:E) RETURN e.t AS t ORDER BY t").len(), 2);
assert_eq!(
db.run("MATCH (e:E) RETURN min(e.t) AS a, max(e.t) AS b")
.len(),
1
);
}