use std::marker::PhantomData;
use std::sync::Arc;
use crate::core::NodeId;
use crate::core::temporal::{TimeRange, Timestamp};
use crate::index::vector::DistanceMetric;
use super::ir::{Predicate, QueryOp, TraversalDepth};
use super::plan::{IndexHint, QueryHints, TemporalContext};
#[derive(Debug, Clone)]
pub struct Query {
pub(crate) ops: Vec<QueryOp>,
pub(crate) temporal_context: Option<TemporalContext>,
pub(crate) hints: QueryHints,
}
impl Query {
#[must_use]
pub fn is_temporal(&self) -> bool {
self.temporal_context.is_some()
}
#[must_use]
pub fn operation_count(&self) -> usize {
self.ops.len()
}
}
pub trait QueryState: private::Sealed {}
mod private {
pub trait Sealed {}
}
pub mod state {
use super::private;
#[derive(Debug, Clone, Copy)]
pub struct Initial;
impl private::Sealed for Initial {}
impl super::QueryState for Initial {}
#[derive(Debug, Clone, Copy)]
pub struct HasNodes;
impl private::Sealed for HasNodes {}
impl super::QueryState for HasNodes {}
#[derive(Debug, Clone, Copy)]
pub struct HasVectorResults;
impl private::Sealed for HasVectorResults {}
impl super::QueryState for HasVectorResults {}
#[derive(Debug, Clone, Copy)]
pub struct HasTraversalResults;
impl private::Sealed for HasTraversalResults {}
impl super::QueryState for HasTraversalResults {}
}
#[derive(Debug, Clone)]
pub struct QueryBuilder<S: QueryState> {
ops: Vec<QueryOp>,
temporal_context: Option<TemporalContext>,
hints: QueryHints,
_phantom: PhantomData<S>,
}
impl QueryBuilder<state::Initial> {
#[must_use]
pub fn new() -> Self {
QueryBuilder {
ops: Vec::new(),
temporal_context: None,
hints: QueryHints::default(),
_phantom: PhantomData,
}
}
#[must_use]
pub fn start(self, node_id: NodeId) -> QueryBuilder<state::HasNodes> {
self.add_op(QueryOp::StartNode(node_id))
}
#[must_use]
pub fn start_from(self, node_ids: Vec<NodeId>) -> QueryBuilder<state::HasNodes> {
self.add_op(QueryOp::StartNodes(node_ids))
}
#[must_use]
pub fn find_similar(
self,
embedding: &[f32],
k: usize,
) -> QueryBuilder<state::HasVectorResults> {
self.add_op(QueryOp::VectorSearch {
embedding: Arc::from(embedding),
k,
metric: DistanceMetric::Cosine,
property_key: None,
})
}
#[must_use]
pub fn find_similar_with_metric(
self,
embedding: &[f32],
k: usize,
metric: DistanceMetric,
) -> QueryBuilder<state::HasVectorResults> {
self.add_op(QueryOp::VectorSearch {
embedding: Arc::from(embedding),
k,
metric,
property_key: None,
})
}
pub fn find_similar_builder(self, embedding: &[f32], k: usize) -> FindSimilarBuilder {
FindSimilarBuilder::new(embedding, k, self)
}
#[must_use]
pub fn scan(self, label: Option<&str>) -> QueryBuilder<state::HasNodes> {
self.add_op(QueryOp::ScanNodes {
label: label.map(String::from),
})
}
#[must_use]
pub fn scan_label(self, label: &str) -> QueryBuilder<state::HasNodes> {
self.add_op(QueryOp::ScanNodes {
label: Some(label.to_string()),
})
}
#[must_use]
pub fn find_by_property(
self,
label: &str,
key: &str,
value: impl Into<super::ir::PredicateValue>,
) -> QueryBuilder<state::HasNodes> {
self.scan_label(label).filter(Predicate::eq(key, value))
}
}
impl Default for QueryBuilder<state::Initial> {
fn default() -> Self {
Self::new()
}
}
impl QueryBuilder<state::HasNodes> {
#[must_use]
pub fn traverse(self, label: &str) -> QueryBuilder<state::HasTraversalResults> {
self.add_op(QueryOp::TraverseOut {
label: Some(label.to_string()),
depth: TraversalDepth::Exact(1),
})
}
#[must_use]
pub fn traverse_all(self) -> QueryBuilder<state::HasTraversalResults> {
self.add_op(QueryOp::TraverseOut {
label: None,
depth: TraversalDepth::Exact(1),
})
}
#[must_use]
pub fn traverse_n(self, label: &str, depth: usize) -> QueryBuilder<state::HasTraversalResults> {
self.add_op(QueryOp::TraverseOut {
label: Some(label.to_string()),
depth: TraversalDepth::Exact(depth),
})
}
#[must_use]
pub fn traverse_in(self, label: &str) -> QueryBuilder<state::HasTraversalResults> {
self.add_op(QueryOp::TraverseIn {
label: Some(label.to_string()),
depth: TraversalDepth::Exact(1),
})
}
#[must_use]
pub fn traverse_both(self, label: &str) -> QueryBuilder<state::HasTraversalResults> {
self.add_op(QueryOp::TraverseBoth {
label: Some(label.to_string()),
depth: TraversalDepth::Exact(1),
})
}
#[must_use]
pub fn rank_by_similarity(
self,
embedding: &[f32],
top_k: usize,
) -> QueryBuilder<state::HasVectorResults> {
self.add_op(QueryOp::RankBySimilarity {
embedding: Arc::from(embedding),
top_k: Some(top_k),
property_key: None,
})
}
pub fn rank_by_similarity_builder(
self,
embedding: &[f32],
top_k: usize,
) -> RankBySimilarityBuilder<state::HasNodes> {
RankBySimilarityBuilder::new(embedding, top_k, self)
}
#[must_use]
pub fn similar_to(
self,
source_node: NodeId,
k: usize,
) -> QueryBuilder<state::HasVectorResults> {
if k == 0 {
panic!("k must be greater than 0");
}
self.add_op(QueryOp::SimilarTo {
source_node,
k,
property_key: None,
label_filter: None,
})
}
pub fn similar_to_builder(
self,
source_node: NodeId,
k: usize,
) -> SimilarToBuilder<state::HasNodes> {
SimilarToBuilder::new(source_node, k, self)
}
#[must_use]
pub fn filter(self, predicate: Predicate) -> QueryBuilder<state::HasNodes> {
self.add_op_same(QueryOp::Filter(predicate))
}
#[must_use]
pub fn with_label(self, label: &str) -> QueryBuilder<state::HasNodes> {
self.add_op_same(QueryOp::FilterLabel(label.to_string()))
}
}
impl QueryBuilder<state::HasTraversalResults> {
#[must_use]
pub fn traverse(self, label: &str) -> QueryBuilder<state::HasTraversalResults> {
self.add_op_same(QueryOp::TraverseOut {
label: Some(label.to_string()),
depth: TraversalDepth::Exact(1),
})
}
#[must_use]
pub fn rank_by_similarity(
self,
embedding: &[f32],
top_k: usize,
) -> QueryBuilder<state::HasVectorResults> {
self.add_op(QueryOp::RankBySimilarity {
embedding: Arc::from(embedding),
top_k: Some(top_k),
property_key: None,
})
}
pub fn rank_by_similarity_builder(
self,
embedding: &[f32],
top_k: usize,
) -> RankBySimilarityBuilder<state::HasTraversalResults> {
RankBySimilarityBuilder::new(embedding, top_k, self)
}
#[must_use]
pub fn filter(self, predicate: Predicate) -> QueryBuilder<state::HasTraversalResults> {
self.add_op_same(QueryOp::Filter(predicate))
}
#[must_use]
pub fn with_label(self, label: &str) -> QueryBuilder<state::HasTraversalResults> {
self.add_op_same(QueryOp::FilterLabel(label.to_string()))
}
}
impl QueryBuilder<state::HasVectorResults> {
#[must_use]
pub fn traverse(self, label: &str) -> QueryBuilder<state::HasTraversalResults> {
self.add_op(QueryOp::TraverseOut {
label: Some(label.to_string()),
depth: TraversalDepth::Exact(1),
})
}
#[must_use]
pub fn filter(self, predicate: Predicate) -> QueryBuilder<state::HasVectorResults> {
self.add_op_same(QueryOp::Filter(predicate))
}
#[must_use]
pub fn with_label(self, label: &str) -> QueryBuilder<state::HasVectorResults> {
self.add_op_same(QueryOp::FilterLabel(label.to_string()))
}
}
impl<S: QueryState> QueryBuilder<S> {
#[must_use]
pub fn as_of(mut self, valid_time: Timestamp, transaction_time: Timestamp) -> Self {
self.temporal_context = Some(TemporalContext::as_of(valid_time, transaction_time));
self
}
#[must_use]
pub fn as_of_valid_time(mut self, valid_time: Timestamp) -> Self {
let mut ctx = self.temporal_context.take().unwrap_or_default();
ctx.valid_time_as_of = Some(valid_time);
self.temporal_context = Some(ctx);
self
}
#[must_use]
pub fn as_of_transaction_time(mut self, transaction_time: Timestamp) -> Self {
let mut ctx = self.temporal_context.take().unwrap_or_default();
ctx.transaction_time_as_of = Some(transaction_time);
self.temporal_context = Some(ctx);
self
}
#[must_use]
pub fn valid_time_between(mut self, start: Timestamp, end: Timestamp) -> Self {
let mut ctx = self.temporal_context.take().unwrap_or_default();
ctx.valid_time_between = Some(TimeRange::between(start, end).unwrap());
self.temporal_context = Some(ctx);
self
}
#[must_use]
pub fn transaction_time_between(mut self, start: Timestamp, end: Timestamp) -> Self {
let mut ctx = self.temporal_context.take().unwrap_or_default();
ctx.transaction_time_between = Some(TimeRange::between(start, end).unwrap());
self.temporal_context = Some(ctx);
self
}
#[must_use]
pub fn between(self, start: Timestamp, end: Timestamp) -> Self {
self.valid_time_between(start, end)
}
#[must_use]
pub fn limit(self, n: usize) -> Self {
self.add_op_same(QueryOp::Limit(n))
}
#[must_use]
pub fn skip(self, n: usize) -> Self {
self.add_op_same(QueryOp::Skip(n))
}
#[must_use]
pub fn with_hint(mut self, hint: IndexHint) -> Self {
self.hints.force_index = Some(hint);
self
}
#[must_use]
pub fn parallel(mut self) -> Self {
self.hints.parallel = true;
self
}
#[must_use]
pub fn with_provenance(mut self) -> Self {
self.hints.include_provenance = true;
self
}
pub fn execute(
self,
db: &crate::AletheiaDB,
) -> crate::core::error::Result<super::executor::QueryResults> {
let query = self.build();
db.execute_query(query)
}
#[must_use]
pub fn build(self) -> Query {
Query {
ops: self.ops,
temporal_context: self.temporal_context,
hints: self.hints,
}
}
fn add_op<T: QueryState>(mut self, op: QueryOp) -> QueryBuilder<T> {
self.ops.push(op);
QueryBuilder {
ops: self.ops,
temporal_context: self.temporal_context,
hints: self.hints,
_phantom: PhantomData,
}
}
fn add_op_same(mut self, op: QueryOp) -> Self {
self.ops.push(op);
self
}
}
#[must_use = "builders do nothing unless you call finish()"]
pub struct SimilarToBuilder<S: QueryState> {
source_node: NodeId,
k: usize,
property_key: Option<String>,
label_filter: Option<String>,
query_builder: QueryBuilder<S>,
}
impl<S: QueryState> SimilarToBuilder<S> {
fn new(source_node: NodeId, k: usize, query_builder: QueryBuilder<S>) -> Self {
if k == 0 {
panic!("k must be greater than 0");
}
SimilarToBuilder {
source_node,
k,
property_key: None,
label_filter: None,
query_builder,
}
}
pub fn property(mut self, key: impl Into<String>) -> Self {
self.property_key = Some(key.into());
self
}
pub fn label_filter(mut self, label: impl Into<String>) -> Self {
self.label_filter = Some(label.into());
self
}
pub fn finish(self) -> QueryBuilder<state::HasVectorResults> {
self.query_builder.add_op(QueryOp::SimilarTo {
source_node: self.source_node,
k: self.k,
property_key: self.property_key,
label_filter: self.label_filter,
})
}
}
#[must_use = "builders do nothing unless you call finish()"]
pub struct RankBySimilarityBuilder<S: QueryState> {
embedding: Arc<[f32]>,
top_k: usize,
property_key: Option<String>,
query_builder: QueryBuilder<S>,
}
impl<S: QueryState> RankBySimilarityBuilder<S> {
fn new(embedding: &[f32], top_k: usize, query_builder: QueryBuilder<S>) -> Self {
RankBySimilarityBuilder {
embedding: Arc::from(embedding),
top_k,
property_key: None,
query_builder,
}
}
pub fn property(mut self, key: impl Into<String>) -> Self {
self.property_key = Some(key.into());
self
}
pub fn finish(self) -> QueryBuilder<state::HasVectorResults> {
self.query_builder.add_op(QueryOp::RankBySimilarity {
embedding: self.embedding,
top_k: Some(self.top_k),
property_key: self.property_key,
})
}
}
#[must_use = "builders do nothing unless you call finish()"]
pub struct FindSimilarBuilder {
embedding: Arc<[f32]>,
k: usize,
metric: DistanceMetric,
property_key: Option<String>,
query_builder: QueryBuilder<state::Initial>,
}
impl FindSimilarBuilder {
fn new(embedding: &[f32], k: usize, query_builder: QueryBuilder<state::Initial>) -> Self {
FindSimilarBuilder {
embedding: Arc::from(embedding),
k,
metric: DistanceMetric::Cosine,
property_key: None,
query_builder,
}
}
pub fn property(mut self, key: impl Into<String>) -> Self {
self.property_key = Some(key.into());
self
}
pub fn metric(mut self, metric: DistanceMetric) -> Self {
self.metric = metric;
self
}
pub fn finish(self) -> QueryBuilder<state::HasVectorResults> {
self.query_builder.add_op(QueryOp::VectorSearch {
embedding: self.embedding,
k: self.k,
metric: self.metric,
property_key: self.property_key,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::NodeId;
fn test_node_id() -> NodeId {
NodeId::new(1).unwrap()
}
fn test_embedding() -> [f32; 4] {
[0.1, 0.2, 0.3, 0.4]
}
#[test]
fn test_simple_node_query() {
let query = QueryBuilder::new().start(test_node_id()).build();
assert_eq!(query.operation_count(), 1);
assert!(!query.is_temporal());
}
#[test]
fn test_traverse_query() {
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.build();
assert_eq!(query.operation_count(), 2);
}
#[test]
fn test_traverse_and_rank() {
let embedding = test_embedding();
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.rank_by_similarity(&embedding, 10)
.build();
assert_eq!(query.operation_count(), 3);
}
#[test]
fn test_vector_search() {
let embedding = test_embedding();
let query = QueryBuilder::new().find_similar(&embedding, 10).build();
assert_eq!(query.operation_count(), 1);
}
#[test]
fn test_temporal_context() {
let query = QueryBuilder::new()
.as_of(1000.into(), 2000.into())
.start(test_node_id())
.build();
assert!(query.is_temporal());
assert!(
query
.temporal_context
.as_ref()
.unwrap()
.as_of_tuple()
.is_some()
);
}
#[test]
fn test_temporal_between() {
let query = QueryBuilder::new()
.between(1000.into(), 2000.into())
.start(test_node_id())
.build();
assert!(query.is_temporal());
assert!(
query
.temporal_context
.as_ref()
.unwrap()
.valid_time_between
.is_some()
);
}
#[test]
fn test_limit_and_skip() {
let query = QueryBuilder::new()
.start(test_node_id())
.skip(10)
.limit(20)
.build();
assert_eq!(query.operation_count(), 3);
}
#[test]
fn test_filter() {
let query = QueryBuilder::new()
.start(test_node_id())
.filter(Predicate::eq("name", "Alice"))
.build();
assert_eq!(query.operation_count(), 2);
}
#[test]
fn test_multi_hop_traversal() {
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.traverse("WORKS_AT")
.build();
assert_eq!(query.operation_count(), 3);
}
#[test]
fn test_scan_with_label() {
let query = QueryBuilder::new().scan_label("Person").build();
assert_eq!(query.operation_count(), 1);
}
#[test]
fn test_full_hybrid_query() {
let embedding = test_embedding();
let query = QueryBuilder::new()
.as_of(1000.into(), 2000.into())
.start(test_node_id())
.traverse("KNOWS")
.rank_by_similarity(&embedding, 10)
.build();
assert!(query.is_temporal());
assert_eq!(query.operation_count(), 3);
}
#[test]
fn test_hints() {
let query = QueryBuilder::new()
.start(test_node_id())
.with_hint(IndexHint::UseVectorIndex)
.parallel()
.build();
assert_eq!(query.hints.force_index, Some(IndexHint::UseVectorIndex));
assert!(query.hints.parallel);
}
#[test]
fn test_chained_filters() {
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.filter(Predicate::eq("status", "active"))
.with_label("Person")
.build();
assert_eq!(query.operation_count(), 4);
}
#[test]
fn test_similar_to_simple() {
let query = QueryBuilder::new()
.start(test_node_id())
.similar_to(test_node_id(), 10)
.build();
assert_eq!(query.operation_count(), 2);
match &query.ops[1] {
QueryOp::SimilarTo {
source_node: _,
k,
property_key,
label_filter,
} => {
assert_eq!(*k, 10);
assert!(property_key.is_none(), "Should use default property");
assert!(label_filter.is_none(), "Should have no label filter");
}
_ => panic!("Expected SimilarTo operation"),
}
}
#[test]
fn test_similar_to_builder_with_property() {
let query = QueryBuilder::new()
.start(test_node_id())
.similar_to_builder(test_node_id(), 10)
.property("custom_embedding")
.finish()
.build();
match &query.ops[1] {
QueryOp::SimilarTo {
property_key,
label_filter,
..
} => {
assert_eq!(property_key.as_deref(), Some("custom_embedding"));
assert!(label_filter.is_none());
}
_ => panic!("Expected SimilarTo operation"),
}
}
#[test]
fn test_similar_to_builder_with_label() {
let query = QueryBuilder::new()
.start(test_node_id())
.similar_to_builder(test_node_id(), 10)
.label_filter("Document")
.finish()
.build();
match &query.ops[1] {
QueryOp::SimilarTo {
property_key,
label_filter,
..
} => {
assert!(property_key.is_none());
assert_eq!(label_filter.as_deref(), Some("Document"));
}
_ => panic!("Expected SimilarTo operation"),
}
}
#[test]
fn test_similar_to_builder_with_all_options() {
let query = QueryBuilder::new()
.start(test_node_id())
.similar_to_builder(test_node_id(), 10)
.property("custom_embedding")
.label_filter("Person")
.finish()
.build();
match &query.ops[1] {
QueryOp::SimilarTo {
property_key,
label_filter,
..
} => {
assert_eq!(property_key.as_deref(), Some("custom_embedding"));
assert_eq!(label_filter.as_deref(), Some("Person"));
}
_ => panic!("Expected SimilarTo operation"),
}
}
#[test]
fn test_similar_to_builder_fluent_chaining() {
let query = QueryBuilder::new()
.start(test_node_id())
.similar_to_builder(test_node_id(), 10)
.property("embedding")
.label_filter("Person")
.finish()
.filter(Predicate::gt("score", 0.8))
.limit(5)
.build();
assert_eq!(query.operation_count(), 4); }
#[test]
#[should_panic(expected = "k must be greater than 0")]
fn test_similar_to_validates_k() {
let _ = QueryBuilder::new()
.start(test_node_id())
.similar_to(test_node_id(), 0); }
#[test]
#[should_panic(expected = "k must be greater than 0")]
fn test_similar_to_builder_validates_k() {
let _ = QueryBuilder::new()
.start(test_node_id())
.similar_to_builder(test_node_id(), 0); }
#[test]
fn test_with_provenance() {
let query = QueryBuilder::new()
.start(test_node_id())
.with_provenance()
.build();
assert!(query.hints.include_provenance);
}
#[test]
fn test_with_provenance_fluent_chaining() {
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.with_provenance()
.limit(10)
.build();
assert!(query.hints.include_provenance);
assert_eq!(query.operation_count(), 3); }
#[test]
fn test_without_provenance_default() {
let query = QueryBuilder::new().start(test_node_id()).build();
assert!(!query.hints.include_provenance);
}
#[test]
fn test_rank_by_similarity_simple_uses_default_property() {
let embedding = test_embedding();
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.rank_by_similarity(&embedding, 10)
.build();
match &query.ops[2] {
QueryOp::RankBySimilarity {
top_k,
property_key,
..
} => {
assert_eq!(*top_k, Some(10));
assert!(property_key.is_none(), "Should use default property");
}
_ => panic!("Expected RankBySimilarity operation"),
}
}
#[test]
fn test_rank_by_similarity_builder_with_property() {
let embedding = test_embedding();
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.rank_by_similarity_builder(&embedding, 10)
.property("custom_embedding")
.finish()
.build();
match &query.ops[2] {
QueryOp::RankBySimilarity { property_key, .. } => {
assert_eq!(property_key.as_deref(), Some("custom_embedding"));
}
_ => panic!("Expected RankBySimilarity operation"),
}
}
#[test]
fn test_rank_by_similarity_builder_fluent_chaining() {
let embedding = test_embedding();
let query = QueryBuilder::new()
.start(test_node_id())
.traverse("KNOWS")
.rank_by_similarity_builder(&embedding, 10)
.property("title_embedding")
.finish()
.filter(Predicate::gt("score", 0.8))
.limit(5)
.build();
assert_eq!(query.operation_count(), 5); }
#[test]
fn test_find_similar_simple_uses_default() {
let embedding = test_embedding();
let query = QueryBuilder::new().find_similar(&embedding, 10).build();
match &query.ops[0] {
QueryOp::VectorSearch { property_key, .. } => {
assert!(property_key.is_none(), "Should use default property");
}
_ => panic!("Expected VectorSearch operation"),
}
}
#[test]
fn test_find_similar_builder_with_property() {
let embedding = test_embedding();
let query = QueryBuilder::new()
.find_similar_builder(&embedding, 10)
.property("body_embedding")
.finish()
.build();
match &query.ops[0] {
QueryOp::VectorSearch { property_key, .. } => {
assert_eq!(property_key.as_deref(), Some("body_embedding"));
}
_ => panic!("Expected VectorSearch operation"),
}
}
#[test]
fn test_find_similar_builder_with_metric() {
use crate::index::vector::DistanceMetric;
let embedding = test_embedding();
let query = QueryBuilder::new()
.find_similar_builder(&embedding, 10)
.metric(DistanceMetric::Euclidean)
.finish()
.build();
match &query.ops[0] {
QueryOp::VectorSearch { metric, .. } => {
assert_eq!(*metric, DistanceMetric::Euclidean);
}
_ => panic!("Expected VectorSearch operation"),
}
}
#[test]
fn test_as_of_valid_time_only() {
use crate::core::hlc::HybridTimestamp;
let ts = HybridTimestamp::new(1000, 0).unwrap();
let query = QueryBuilder::new()
.as_of_valid_time(ts)
.start(test_node_id())
.build();
assert!(query.is_temporal());
let ctx = query.temporal_context.as_ref().unwrap();
assert_eq!(ctx.valid_time_as_of, Some(ts));
assert_eq!(ctx.transaction_time_as_of, None); }
#[test]
fn test_as_of_transaction_time_only() {
use crate::core::hlc::HybridTimestamp;
let ts = HybridTimestamp::new(1000, 0).unwrap();
let query = QueryBuilder::new()
.as_of_transaction_time(ts)
.start(test_node_id())
.build();
assert!(query.is_temporal());
let ctx = query.temporal_context.as_ref().unwrap();
assert_eq!(ctx.transaction_time_as_of, Some(ts));
assert_eq!(ctx.valid_time_as_of, None); }
#[test]
fn test_valid_time_between_method() {
use crate::core::hlc::HybridTimestamp;
let start = HybridTimestamp::new(1000, 0).unwrap();
let end = HybridTimestamp::new(2000, 0).unwrap();
let query = QueryBuilder::new()
.valid_time_between(start, end)
.start(test_node_id())
.build();
assert!(query.is_temporal());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.valid_time_between.is_some());
assert!(ctx.transaction_time_between.is_none());
let range = ctx.valid_time_between.as_ref().unwrap();
assert_eq!(range.start(), start);
assert_eq!(range.end(), end);
}
#[test]
fn test_transaction_time_between_method() {
use crate::core::hlc::HybridTimestamp;
let start = HybridTimestamp::new(1000, 0).unwrap();
let end = HybridTimestamp::new(2000, 0).unwrap();
let query = QueryBuilder::new()
.transaction_time_between(start, end)
.start(test_node_id())
.build();
assert!(query.is_temporal());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.transaction_time_between.is_some());
assert!(ctx.valid_time_between.is_none());
let range = ctx.transaction_time_between.as_ref().unwrap();
assert_eq!(range.start(), start);
assert_eq!(range.end(), end);
}
#[test]
fn test_combine_both_dimensions_with_as_of() {
use crate::core::hlc::HybridTimestamp;
let valid_ts = HybridTimestamp::new(1000, 0).unwrap();
let tx_ts = HybridTimestamp::new(2000, 0).unwrap();
let query = QueryBuilder::new()
.as_of(valid_ts, tx_ts)
.start(test_node_id())
.build();
assert!(query.is_temporal());
let ctx = query.temporal_context.as_ref().unwrap();
assert_eq!(ctx.valid_time_as_of, Some(valid_ts));
assert_eq!(ctx.transaction_time_as_of, Some(tx_ts));
}
#[test]
fn test_combine_both_dimensions_independently() {
use crate::core::hlc::HybridTimestamp;
let valid_ts = HybridTimestamp::new(1000, 0).unwrap();
let tx_ts = HybridTimestamp::new(2000, 0).unwrap();
let query = QueryBuilder::new()
.as_of_valid_time(valid_ts)
.as_of_transaction_time(tx_ts)
.start(test_node_id())
.build();
assert!(query.is_temporal());
let ctx = query.temporal_context.as_ref().unwrap();
assert_eq!(ctx.valid_time_as_of, Some(valid_ts));
assert_eq!(ctx.transaction_time_as_of, Some(tx_ts));
}
#[test]
fn test_temporal_methods_fluent_chaining() {
use crate::core::hlc::HybridTimestamp;
let valid_ts = HybridTimestamp::new(1000, 0).unwrap();
let query = QueryBuilder::new()
.as_of_valid_time(valid_ts)
.start(test_node_id())
.traverse("KNOWS")
.filter(Predicate::eq("status", "active"))
.limit(10)
.build();
assert!(query.is_temporal());
assert_eq!(query.operation_count(), 4); }
#[test]
fn test_temporal_method_overwrites_previous() {
use crate::core::hlc::HybridTimestamp;
let ts1 = HybridTimestamp::new(1000, 0).unwrap();
let ts2 = HybridTimestamp::new(2000, 0).unwrap();
let query = QueryBuilder::new()
.as_of_valid_time(ts1)
.as_of_valid_time(ts2) .start(test_node_id())
.build();
let ctx = query.temporal_context.as_ref().unwrap();
assert_eq!(ctx.valid_time_as_of, Some(ts2)); }
#[test]
fn test_mixed_range_and_point_queries() {
use crate::core::hlc::HybridTimestamp;
let point_ts = HybridTimestamp::new(1500, 0).unwrap();
let range_start = HybridTimestamp::new(1000, 0).unwrap();
let range_end = HybridTimestamp::new(2000, 0).unwrap();
let query = QueryBuilder::new()
.as_of_valid_time(point_ts)
.transaction_time_between(range_start, range_end)
.start(test_node_id())
.build();
let ctx = query.temporal_context.as_ref().unwrap();
assert_eq!(ctx.valid_time_as_of, Some(point_ts));
assert!(ctx.transaction_time_between.is_some());
let tx_range = ctx.transaction_time_between.as_ref().unwrap();
assert_eq!(tx_range.start(), range_start);
assert_eq!(tx_range.end(), range_end);
}
#[test]
fn test_find_by_property_produces_correct_ops() {
let query = QueryBuilder::new()
.find_by_property("Person", "name", "Alice")
.build();
assert_eq!(query.ops.len(), 2);
assert!(matches!(
&query.ops[0],
QueryOp::ScanNodes { label: Some(l) } if l == "Person"
));
assert!(matches!(
&query.ops[1],
QueryOp::Filter(Predicate::Eq { key, .. }) if key == "name"
));
}
#[test]
fn test_find_by_property_with_int_value() {
let query = QueryBuilder::new()
.find_by_property("Person", "age", 30i64)
.build();
assert_eq!(query.ops.len(), 2);
assert!(matches!(
&query.ops[0],
QueryOp::ScanNodes { label: Some(l) } if l == "Person"
));
assert!(matches!(
&query.ops[1],
QueryOp::Filter(Predicate::Eq { key, value })
if key == "age" && matches!(value, super::super::ir::PredicateValue::Int(30))
));
}
}