use std::sync::Arc;
use rushdb_lmpg::plan::FieldChange;
use rushdb_lmpg::Lmpg;
use rushdb_query::document::parse_document;
use rushdb_query::{aggregate, find_records, AggregateRequest, TimeBucketSpec, TimeUnit};
use rushdb_storage::Store;
use rushdb_types::ids::TenantId;
use serde_json::{json, Value};
fn setup() -> Lmpg {
let store = Arc::new(Store::new_in_memory());
Lmpg::new(store, TenantId::from_ordinal(81))
}
fn run(lmpg: &Lmpg, document: &Value) -> Vec<Value> {
let q = parse_document(document).expect("valid query document");
if let Some(agg) = q.aggregate {
let buckets = aggregate(lmpg, &agg).expect("aggregate");
buckets
.iter()
.map(|b| Value::Object(b.keys.clone()))
.collect()
} else {
find_records(lmpg, &q.search).expect("search").records
}
}
const T: i64 = 1_672_628_645_678;
#[test]
fn microsecond_and_nanosecond_component_filters() {
let lmpg = setup();
lmpg.create_record(
"Event",
None,
vec![
("at".into(), FieldChange::Set(json!(T))),
("name".into(), FieldChange::Set(json!("exact"))),
],
)
.unwrap();
lmpg.create_record(
"Event",
None,
vec![
("at".into(), FieldChange::Set(json!(T + 1))),
("name".into(), FieldChange::Set(json!("next"))),
],
)
.unwrap();
for (op, val, want_name) in [
("$microsecond", 678_000i64, "exact"),
("$nanosecond", 678_000_000i64, "exact"),
] {
let mut op_map = serde_json::Map::new();
op_map.insert(op.to_string(), json!(val));
let document = json!({
"labels": ["Event"],
"where": { "at": serde_json::Value::Object(op_map) }
});
let records = run(&lmpg, &document);
assert_eq!(records.len(), 1, "{op} must match exactly one record");
assert_eq!(
records[0].get("name"),
Some(&json!(want_name)),
"{op} matched the wrong record"
);
}
let document = json!({
"labels": ["Event"],
"where": { "at": { "$microsecond": 123_000 } }
});
assert_eq!(run(&lmpg, &document).len(), 0);
}
#[test]
fn datetime_component_corpus() {
let lmpg = setup();
lmpg.create_record(
"Event",
None,
vec![
("at".into(), FieldChange::Set(json!(T))),
("name".into(), FieldChange::Set(json!("t"))),
],
)
.unwrap();
let cases = [
("$year", 2023),
("$month", 1),
("$day", 2),
("$hour", 3),
("$minute", 4),
("$second", 5),
("$millisecond", 678),
("$microsecond", 678_000),
("$nanosecond", 678_000_000),
];
for (op, val) in cases {
let document = json!({
"labels": ["Event"],
"where": { "at": { op: val } }
});
let records = run(&lmpg, &document);
assert_eq!(records.len(), 1, "{op}={val} must match the record");
}
for (op, _) in cases {
let document = json!({
"labels": ["Event"],
"where": { "at": { op: 1_999_999_999 } }
});
assert_eq!(
run(&lmpg, &document).len(),
0,
"{op} must not match a 1999 value"
);
}
}
#[test]
fn time_bucket_output_naming_honors_alias() {
let lmpg = setup();
lmpg.create_record(
"Event",
None,
vec![
("at".into(), FieldChange::Set(json!(1_600_000_000_000i64))),
("v".into(), FieldChange::Set(json!(1.0))),
],
)
.unwrap();
lmpg.create_record(
"Event",
None,
vec![
("at".into(), FieldChange::Set(json!(1_600_000_100_000i64))),
("v".into(), FieldChange::Set(json!(2.0))),
],
)
.unwrap();
let buckets = aggregate(
&lmpg,
&AggregateRequest {
labels: vec!["Event".into()],
time_bucket: Some(TimeBucketSpec::fixed("at", TimeUnit::Days)),
time_bucket_alias: Some("day_bucket".into()),
..Default::default()
},
)
.unwrap();
assert_eq!(buckets.len(), 1);
assert!(
buckets[0].keys.contains_key("day_bucket"),
"alias must be the output key, got {:?}",
buckets[0].keys.keys().collect::<Vec<_>>()
);
assert!(
!buckets[0].keys.keys().any(|k| k.starts_with("__bucket_")),
"internal bucket key must not leak, got {:?}",
buckets[0].keys.keys().collect::<Vec<_>>()
);
let buckets = aggregate(
&lmpg,
&AggregateRequest {
labels: vec!["Event".into()],
time_bucket: Some(TimeBucketSpec::fixed("at", TimeUnit::Days)),
..Default::default()
},
)
.unwrap();
let key = buckets[0]
.keys
.keys()
.find(|k| k.starts_with("__bucket_at/"))
.cloned()
.unwrap();
assert_eq!(
buckets[0].keys.get(&key),
Some(&json!(1_599_955_200_000u64))
);
}
#[test]
fn query_document_time_bucket_alias_propagates() {
let lmpg = setup();
lmpg.create_record(
"Event",
None,
vec![
("at".into(), FieldChange::Set(json!(1_600_000_000_000i64))),
("v".into(), FieldChange::Set(json!(1.0))),
],
)
.unwrap();
let buckets = run(
&lmpg,
&json!({
"labels": ["Event"],
"select": {
"monthly": { "$timeBucket": { "field": "at", "unit": "month" } },
"total": { "$sum": "v" }
},
"groupBy": []
}),
);
assert_eq!(buckets.len(), 1);
assert!(
buckets[0].get("monthly").is_some(),
"alias must be the output key, got {:?}",
buckets[0]
);
}