use std::sync::Arc;
use crate::core::NodeId;
use crate::core::temporal::{TimeRange, Timestamp};
use crate::index::vector::DistanceMetric;
use super::ir::{Direction, Predicate, TraversalDepth};
#[derive(Debug, Clone, PartialEq)]
pub struct LogicalPlan {
pub root: LogicalOp,
pub temporal_context: Option<TemporalContext>,
pub hints: QueryHints,
}
impl LogicalPlan {
#[must_use]
pub fn new(root: LogicalOp) -> Self {
LogicalPlan {
root,
temporal_context: None,
hints: QueryHints::default(),
}
}
#[must_use]
pub fn with_temporal_context(mut self, context: TemporalContext) -> Self {
self.temporal_context = Some(context);
self
}
#[must_use]
pub fn with_hints(mut self, hints: QueryHints) -> Self {
self.hints = hints;
self
}
#[must_use]
pub fn is_temporal(&self) -> bool {
self.temporal_context.is_some()
}
#[must_use]
pub fn has_vector_ops(&self) -> bool {
self.root.has_vector_ops()
}
#[must_use]
pub fn has_traversal(&self) -> bool {
self.root.has_traversal()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LogicalOp {
Scan(ScanOp),
Unary {
op: UnaryOp,
input: Box<LogicalOp>,
},
Binary {
op: BinaryOp,
left: Box<LogicalOp>,
right: Box<LogicalOp>,
},
Empty,
}
impl LogicalOp {
#[must_use]
pub fn unary(op: UnaryOp, input: LogicalOp) -> Self {
LogicalOp::Unary {
op,
input: Box::new(input),
}
}
#[must_use]
pub fn binary(op: BinaryOp, left: LogicalOp, right: LogicalOp) -> Self {
LogicalOp::Binary {
op,
left: Box::new(left),
right: Box::new(right),
}
}
#[must_use]
pub fn has_vector_ops(&self) -> bool {
match self {
LogicalOp::Scan(scan) => matches!(scan, ScanOp::VectorSearch { .. }),
LogicalOp::Unary { op, input } => {
matches!(op, UnaryOp::VectorRank { .. }) || input.has_vector_ops()
}
LogicalOp::Binary { left, right, .. } => {
left.has_vector_ops() || right.has_vector_ops()
}
LogicalOp::Empty => false,
}
}
#[must_use]
pub fn has_traversal(&self) -> bool {
match self {
LogicalOp::Scan(_) => false,
LogicalOp::Unary { op, input } => {
matches!(op, UnaryOp::Traverse { .. }) || input.has_traversal()
}
LogicalOp::Binary { left, right, .. } => left.has_traversal() || right.has_traversal(),
LogicalOp::Empty => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScanOp {
NodeLookup(Vec<NodeId>),
NodeScan {
label: Option<String>,
estimated_rows: Option<usize>,
},
VectorSearch {
embedding: Arc<[f32]>,
k: usize,
label_filter: Option<String>,
metric: DistanceMetric,
property_key: Option<String>,
},
TemporalNodeLookup {
node_ids: Vec<NodeId>,
valid_time: Timestamp,
transaction_time: Timestamp,
},
TemporalVectorSearch {
embedding: Arc<[f32]>,
k: usize,
timestamp: Timestamp,
property_key: Option<String>,
},
SimilarToNode {
source_node: NodeId,
property_key: String,
k: usize,
label_filter: Option<String>,
},
EdgeScan {
edge_type: Option<String>,
estimated_rows: Option<usize>,
},
PropertyScan {
label: String,
key: String,
value: super::ir::PredicateValue,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum UnaryOp {
Filter(Predicate),
Project(Vec<String>),
Limit(usize),
Skip(usize),
Traverse {
direction: Direction,
label: Option<String>,
depth: TraversalDepth,
},
VectorRank {
embedding: Arc<[f32]>,
top_k: Option<usize>,
property_key: Option<String>,
},
Sort {
key: SortKey,
descending: bool,
},
TemporalTrack {
time_range: TimeRange,
},
Distinct,
Count,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BinaryOp {
Union,
Intersect,
Except,
Join {
left_key: String,
right_key: String,
},
}
pub use super::ir::SortKey;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TemporalContext {
pub valid_time_as_of: Option<Timestamp>,
pub transaction_time_as_of: Option<Timestamp>,
pub valid_time_between: Option<TimeRange>,
pub transaction_time_between: Option<TimeRange>,
pub include_history: bool,
}
impl TemporalContext {
#[must_use]
pub fn as_of(valid_time: Timestamp, transaction_time: Timestamp) -> Self {
TemporalContext {
valid_time_as_of: Some(valid_time),
transaction_time_as_of: Some(transaction_time),
valid_time_between: None,
transaction_time_between: None,
include_history: false,
}
}
#[must_use]
pub fn as_of_valid_time(timestamp: Timestamp) -> Self {
TemporalContext {
valid_time_as_of: Some(timestamp),
transaction_time_as_of: None,
valid_time_between: None,
transaction_time_between: None,
include_history: false,
}
}
#[must_use]
pub fn as_of_transaction_time(timestamp: Timestamp) -> Self {
TemporalContext {
valid_time_as_of: None,
transaction_time_as_of: Some(timestamp),
valid_time_between: None,
transaction_time_between: None,
include_history: false,
}
}
#[must_use]
pub fn valid_time_between(range: TimeRange) -> Self {
TemporalContext {
valid_time_as_of: None,
transaction_time_as_of: None,
valid_time_between: Some(range),
transaction_time_between: None,
include_history: false,
}
}
#[must_use]
pub fn transaction_time_between(range: TimeRange) -> Self {
TemporalContext {
valid_time_as_of: None,
transaction_time_as_of: None,
valid_time_between: None,
transaction_time_between: Some(range),
include_history: false,
}
}
#[must_use]
#[deprecated(
since = "0.1.0",
note = "Use valid_time_between() or transaction_time_between() instead"
)]
pub fn between(range: TimeRange) -> Self {
Self::valid_time_between(range)
}
#[must_use]
pub fn with_history(mut self, include: bool) -> Self {
self.include_history = include;
self
}
#[must_use]
pub fn resolve_now(&self) -> (Timestamp, Timestamp) {
use crate::core::temporal::time;
let now = time::now();
let valid_time = self.valid_time_as_of.unwrap_or(now);
let transaction_time = self.transaction_time_as_of.unwrap_or(now);
(valid_time, transaction_time)
}
#[must_use]
pub fn as_of_tuple(&self) -> Option<(Timestamp, Timestamp)> {
match (self.valid_time_as_of, self.transaction_time_as_of) {
(Some(vt), Some(tt)) => Some((vt, tt)),
_ => None,
}
}
#[must_use]
pub fn is_temporal(&self) -> bool {
self.valid_time_as_of.is_some()
|| self.transaction_time_as_of.is_some()
|| self.valid_time_between.is_some()
|| self.transaction_time_between.is_some()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct QueryHints {
pub estimated_cardinality: Option<usize>,
pub force_index: Option<IndexHint>,
pub disabled_optimizations: Vec<String>,
pub parallel: bool,
pub include_provenance: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IndexHint {
UseVectorIndex,
UseTemporalIndex,
UseCurrentStorage,
UseHistoricalStorage,
UseBruteForce,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logical_plan_creation() {
let plan = LogicalPlan::new(LogicalOp::Scan(ScanOp::NodeLookup(vec![])));
assert!(!plan.is_temporal());
assert!(!plan.has_vector_ops());
assert!(!plan.has_traversal());
}
#[test]
fn test_logical_plan_with_temporal() {
let plan = LogicalPlan::new(LogicalOp::Scan(ScanOp::NodeLookup(vec![])))
.with_temporal_context(TemporalContext::as_of(1000.into(), 1000.into()));
assert!(plan.is_temporal());
}
#[test]
fn test_has_vector_ops() {
let vector_scan = LogicalOp::Scan(ScanOp::VectorSearch {
embedding: Arc::from([1.0f32; 384].as_slice()),
k: 10,
label_filter: None,
metric: DistanceMetric::Cosine,
property_key: None,
});
assert!(vector_scan.has_vector_ops());
let vector_rank = LogicalOp::unary(
UnaryOp::VectorRank {
embedding: Arc::from([1.0f32; 384].as_slice()),
top_k: Some(10),
property_key: None,
},
LogicalOp::Scan(ScanOp::NodeLookup(vec![])),
);
assert!(vector_rank.has_vector_ops());
}
#[test]
fn test_has_traversal() {
let traverse = LogicalOp::unary(
UnaryOp::Traverse {
direction: Direction::Outgoing,
label: None,
depth: TraversalDepth::Exact(1),
},
LogicalOp::Scan(ScanOp::NodeLookup(vec![])),
);
assert!(traverse.has_traversal());
}
#[test]
fn test_temporal_context_backward_compat() {
let as_of = TemporalContext::as_of(1000.into(), 2000.into());
assert_eq!(as_of.valid_time_as_of, Some(1000.into()));
assert_eq!(as_of.transaction_time_as_of, Some(2000.into()));
assert_eq!(as_of.valid_time_between, None);
assert_eq!(as_of.transaction_time_between, None);
#[allow(deprecated)]
let between = TemporalContext::between(TimeRange::from(0.into()));
assert_eq!(between.valid_time_as_of, None);
assert_eq!(between.transaction_time_as_of, None);
assert!(between.valid_time_between.is_some());
}
#[test]
fn test_temporal_context_as_of_valid_time_only() {
use crate::core::temporal::time;
let ts = time::now();
let ctx = TemporalContext::as_of_valid_time(ts);
assert_eq!(ctx.valid_time_as_of, Some(ts));
assert_eq!(ctx.transaction_time_as_of, None);
assert_eq!(ctx.valid_time_between, None);
assert_eq!(ctx.transaction_time_between, None);
assert!(!ctx.include_history);
}
#[test]
fn test_temporal_context_as_of_transaction_time_only() {
use crate::core::temporal::time;
let ts = time::now();
let ctx = TemporalContext::as_of_transaction_time(ts);
assert_eq!(ctx.transaction_time_as_of, Some(ts));
assert_eq!(ctx.valid_time_as_of, None);
assert_eq!(ctx.valid_time_between, None);
assert_eq!(ctx.transaction_time_between, None);
assert!(!ctx.include_history);
}
#[test]
fn test_temporal_context_as_of_both_dimensions() {
use crate::core::temporal::time;
let vt = time::now();
let tt = time::now();
let ctx = TemporalContext::as_of(vt, tt);
assert_eq!(ctx.valid_time_as_of, Some(vt));
assert_eq!(ctx.transaction_time_as_of, Some(tt));
assert_eq!(ctx.valid_time_between, None);
assert_eq!(ctx.transaction_time_between, None);
}
#[test]
fn test_temporal_context_valid_time_between() {
use crate::core::hlc::HybridTimestamp;
let start = HybridTimestamp::new(1000, 0).unwrap();
let end = HybridTimestamp::new(2000, 0).unwrap();
let range = TimeRange::new(start, end).unwrap();
let ctx = TemporalContext::valid_time_between(range);
assert_eq!(ctx.valid_time_between, Some(range));
assert_eq!(ctx.transaction_time_between, None);
assert_eq!(ctx.valid_time_as_of, None);
assert_eq!(ctx.transaction_time_as_of, None);
}
#[test]
fn test_temporal_context_transaction_time_between() {
use crate::core::hlc::HybridTimestamp;
let start = HybridTimestamp::new(1000, 0).unwrap();
let end = HybridTimestamp::new(2000, 0).unwrap();
let range = TimeRange::new(start, end).unwrap();
let ctx = TemporalContext::transaction_time_between(range);
assert_eq!(ctx.transaction_time_between, Some(range));
assert_eq!(ctx.valid_time_between, None);
assert_eq!(ctx.valid_time_as_of, None);
assert_eq!(ctx.transaction_time_as_of, None);
}
#[test]
fn test_temporal_context_resolve_now_fills_missing() {
use crate::core::hlc::HybridTimestamp;
let ts = HybridTimestamp::new(1000, 0).unwrap();
let ctx = TemporalContext::as_of_valid_time(ts);
let resolved = ctx.resolve_now();
assert_eq!(resolved.0, ts);
assert!(resolved.1.wallclock() > ts.wallclock());
}
#[test]
fn test_temporal_context_resolve_now_preserves_both() {
use crate::core::hlc::HybridTimestamp;
let vt = HybridTimestamp::new(1000, 0).unwrap();
let tt = HybridTimestamp::new(2000, 0).unwrap();
let ctx = TemporalContext::as_of(vt, tt);
let resolved = ctx.resolve_now();
assert_eq!(resolved.0, vt);
assert_eq!(resolved.1, tt);
}
#[test]
fn test_temporal_context_with_history() {
use crate::core::temporal::time;
let ts = time::now();
let ctx = TemporalContext::as_of_valid_time(ts).with_history(true);
assert!(ctx.include_history);
assert_eq!(ctx.valid_time_as_of, Some(ts));
}
#[test]
fn test_temporal_context_default_is_empty() {
let ctx = TemporalContext::default();
assert_eq!(ctx.valid_time_as_of, None);
assert_eq!(ctx.transaction_time_as_of, None);
assert_eq!(ctx.valid_time_between, None);
assert_eq!(ctx.transaction_time_between, None);
assert!(!ctx.include_history);
}
}