//! Temporal Query Support: As-Of-Date Queries //! //! Query memory state at a specific point in time. //! Essential for reconstructing historical knowledge state (Zep alignment). //! //! CRAP: 12 (Temporal filtering logic) //! SOLID: Single responsibility (temporal queries) //! DRY: Reuses query types from mem_core use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use tracing::{debug, info}; /// Temporal query configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemporalQueryConfig { pub enabled: bool, pub allow_future_dates: bool, // Allow querying past future dates pub default_to_now: bool, // If no time specified, use NOW() pub max_lookback_days: Option, // Limit how far back to query } impl Default for TemporalQueryConfig { fn default() -> Self { Self { enabled: true, allow_future_dates: false, default_to_now: true, max_lookback_days: Some(365 * 5), // 5 years } } } /// Temporal query specification #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemporalQuery { /// Base query text pub query: String, /// Point in time to query at pub as_of_time: DateTime, /// Optional: time range for temporal search pub time_range: Option<(DateTime, DateTime)>, } /// Temporal query result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemporalQueryResult { pub query: String, pub as_of_time: DateTime, pub num_facts: usize, pub valid_facts: usize, // Facts valid at as_of_time pub invalid_facts: usize, // Facts invalid at as_of_time pub note: String, } /// Temporal filter for edges #[derive(Debug, Clone)] pub struct TemporalFilter { config: TemporalQueryConfig, } impl TemporalFilter { pub fn new(config: TemporalQueryConfig) -> Self { Self { config } } /// Validate query time pub fn validate_query_time(&self, time: DateTime) -> Result<(), String> { if !self.config.enabled { return Ok(()); } let now = Utc::now(); // Check if querying future if !self.config.allow_future_dates && time > now { return Err(format!( "Cannot query future time: {} (now: {})", time, now )); } // Check lookback limit if let Some(max_days) = self.config.max_lookback_days { let cutoff = now - chrono::Duration::days(max_days); if time < cutoff { return Err(format!( "Query time {} exceeds max lookback of {} days", time, max_days )); } } Ok(()) } /// Check if edge is valid at point in time /// Returns: (is_valid_at_time, is_expired_at_time) pub fn is_edge_valid_at_time( &self, t_valid: Option>, t_invalid: Option>, query_time: DateTime, ) -> (bool, bool) { if !self.config.enabled { return (true, false); } // Edge is valid if: // - t_valid is None or <= query_time (became true at/before query time) // - t_invalid is None or > query_time (didn't become false before query time) let is_valid = (t_valid.is_none() || t_valid.unwrap() <= query_time) && (t_invalid.is_none() || t_invalid.unwrap() > query_time); let is_expired = t_invalid.is_some() && t_invalid.unwrap() <= query_time; (is_valid, is_expired) } /// Get SQL WHERE clause for temporal filtering pub fn sql_where_clause( &self, query_time: DateTime, table_prefix: &str, ) -> String { if !self.config.enabled { return format!("{}.t_expired IS NULL", table_prefix); } format!( "({p}.t_valid IS NULL OR {p}.t_valid <= '{time}') AND \ ({p}.t_invalid IS NULL OR {p}.t_invalid > '{time}') AND \ {p}.t_expired IS NULL", p = table_prefix, time = query_time.to_rfc3339() ) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_temporal_config_defaults() { let config = TemporalQueryConfig::default(); assert!(config.enabled); assert!(!config.allow_future_dates); assert!(config.default_to_now); assert_eq!(config.max_lookback_days, Some(365 * 5)); } #[test] fn test_validate_query_time_now() { let config = TemporalQueryConfig::default(); let filter = TemporalFilter::new(config); let now = Utc::now(); assert!(filter.validate_query_time(now).is_ok()); } #[test] fn test_validate_query_time_past() { let config = TemporalQueryConfig::default(); let filter = TemporalFilter::new(config); let past = Utc::now() - chrono::Duration::days(30); assert!(filter.validate_query_time(past).is_ok()); } #[test] fn test_validate_query_time_future_disallowed() { let config = TemporalQueryConfig { allow_future_dates: false, ..Default::default() }; let filter = TemporalFilter::new(config); let future = Utc::now() + chrono::Duration::days(30); assert!(filter.validate_query_time(future).is_err()); } #[test] fn test_validate_query_time_future_allowed() { let config = TemporalQueryConfig { allow_future_dates: true, ..Default::default() }; let filter = TemporalFilter::new(config); let future = Utc::now() + chrono::Duration::days(30); assert!(filter.validate_query_time(future).is_ok()); } #[test] fn test_is_edge_valid_at_time_current() { let config = TemporalQueryConfig::default(); let filter = TemporalFilter::new(config); let now = Utc::now(); let past = now - chrono::Duration::days(10); // Edge valid from past, still active let (is_valid, is_expired) = filter.is_edge_valid_at_time(Some(past), None, now); assert!(is_valid); assert!(!is_expired); } #[test] fn test_is_edge_valid_at_time_expired() { let config = TemporalQueryConfig::default(); let filter = TemporalFilter::new(config); let now = Utc::now(); let past = now - chrono::Duration::days(10); let future = now + chrono::Duration::days(10); // Edge valid from past, became invalid before now let (is_valid, is_expired) = filter.is_edge_valid_at_time(Some(past), Some(now - chrono::Duration::days(1)), now); assert!(!is_valid); assert!(is_expired); } #[test] fn test_is_edge_valid_at_time_historical() { let config = TemporalQueryConfig::default(); let filter = TemporalFilter::new(config); let now = Utc::now(); let past_30 = now - chrono::Duration::days(30); let past_10 = now - chrono::Duration::days(10); let past_5 = now - chrono::Duration::days(5); // Query at 30 days ago: edge didn't exist yet let (is_valid, _) = filter.is_edge_valid_at_time(Some(past_10), Some(past_5), past_30); assert!(!is_valid); // Query at 8 days ago: edge was valid let (is_valid, _) = filter.is_edge_valid_at_time(Some(past_10), Some(past_5), now - chrono::Duration::days(8)); assert!(is_valid); } #[test] fn test_sql_where_clause() { let config = TemporalQueryConfig::default(); let filter = TemporalFilter::new(config); let now = Utc::now(); let clause = filter.sql_where_clause(now, "e"); assert!(clause.contains("e.t_valid IS NULL OR e.t_valid <=")); assert!(clause.contains("e.t_invalid IS NULL OR e.t_invalid >")); assert!(clause.contains("e.t_expired IS NULL")); } #[test] fn test_sql_where_clause_disabled() { let config = TemporalQueryConfig { enabled: false, ..Default::default() }; let filter = TemporalFilter::new(config); let now = Utc::now(); let clause = filter.sql_where_clause(now, "e"); // When disabled, only check t_expired assert_eq!(clause, "e.t_expired IS NULL"); } }