use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
pub type DocumentId = String;
pub type PeerId = String;
pub type Timestamp = SystemTime;
pub use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
pub id: Option<DocumentId>,
pub fields: HashMap<String, Value>,
pub updated_at: Timestamp,
}
impl Document {
pub fn new(fields: HashMap<String, Value>) -> Self {
Self {
id: None,
fields,
updated_at: SystemTime::now(),
}
}
pub fn with_id(id: impl Into<String>, fields: HashMap<String, Value>) -> Self {
Self {
id: Some(id.into()),
fields,
updated_at: SystemTime::now(),
}
}
pub fn get(&self, field: &str) -> Option<&Value> {
self.fields.get(field)
}
pub fn set(&mut self, field: impl Into<String>, value: Value) {
self.fields.insert(field.into(), value);
self.updated_at = SystemTime::now();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct GeoPoint {
pub lat: f64,
pub lon: f64,
}
impl GeoPoint {
pub fn new(lat: f64, lon: f64) -> Self {
Self { lat, lon }
}
pub fn distance_to(&self, other: &GeoPoint) -> f64 {
haversine_distance(self.lat, self.lon, other.lat, other.lon)
}
pub fn within_bounds(&self, min: &GeoPoint, max: &GeoPoint) -> bool {
self.lat >= min.lat && self.lat <= max.lat && self.lon >= min.lon && self.lon <= max.lon
}
pub fn within_radius(&self, center: &GeoPoint, radius_meters: f64) -> bool {
self.distance_to(center) <= radius_meters
}
}
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
const EARTH_RADIUS_METERS: f64 = 6_371_000.0;
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let delta_lat = (lat2 - lat1).to_radians();
let delta_lon = (lon2 - lon1).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
EARTH_RADIUS_METERS * c
}
#[derive(Debug, Clone)]
pub enum Query {
Eq { field: String, value: Value },
Lt { field: String, value: Value },
Gt { field: String, value: Value },
And(Vec<Query>),
Or(Vec<Query>),
Not(Box<Query>),
All,
Custom(String),
IncludeDeleted(Box<Query>),
DeletedOnly,
WithinRadius {
center: GeoPoint,
radius_meters: f64,
lat_field: Option<String>,
lon_field: Option<String>,
},
WithinBounds {
min: GeoPoint,
max: GeoPoint,
lat_field: Option<String>,
lon_field: Option<String>,
},
}
impl Query {
pub fn includes_deleted(&self) -> bool {
matches!(self, Query::IncludeDeleted(_) | Query::DeletedOnly)
}
pub fn is_deleted_only(&self) -> bool {
matches!(self, Query::DeletedOnly)
}
pub fn with_deleted(self) -> Self {
if self.includes_deleted() {
self
} else {
Query::IncludeDeleted(Box::new(self))
}
}
pub fn inner_query(&self) -> &Query {
match self {
Query::IncludeDeleted(inner) => inner.as_ref(),
other => other,
}
}
pub fn matches_deletion_state(&self, doc: &Document) -> bool {
let is_deleted = doc
.fields
.get("_deleted")
.and_then(|v| v.as_bool())
.unwrap_or(false);
match self {
Query::DeletedOnly => is_deleted,
Query::IncludeDeleted(_) => true, _ => !is_deleted, }
}
}
pub struct ChangeStream {
pub receiver: tokio::sync::mpsc::UnboundedReceiver<ChangeEvent>,
}
#[derive(Debug, Clone)]
pub enum ChangeEvent {
Updated {
collection: String,
document: Document,
},
Removed {
collection: String,
doc_id: DocumentId,
},
Initial { documents: Vec<Document> },
}
#[derive(Debug, Clone)]
pub struct PeerInfo {
pub peer_id: PeerId,
pub address: Option<String>,
pub transport: TransportType,
pub connected: bool,
pub last_seen: Timestamp,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportType {
Tcp,
Bluetooth,
#[serde(rename = "mdns")]
Mdns,
WebSocket,
Custom,
}
#[derive(Debug, Clone)]
pub enum PeerEvent {
Discovered(PeerInfo),
Connected(PeerInfo),
Disconnected {
peer_id: PeerId,
reason: Option<String>,
},
Lost(PeerId),
}
#[derive(Debug, Clone)]
pub struct BackendConfig {
pub app_id: String,
pub persistence_dir: PathBuf,
pub shared_key: Option<String>,
pub transport: TransportConfig,
pub extra: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct TransportConfig {
pub tcp_listen_port: Option<u16>,
pub tcp_connect_address: Option<String>,
pub enable_mdns: bool,
pub enable_bluetooth: bool,
pub enable_websocket: bool,
pub custom: HashMap<String, String>,
}
impl Default for TransportConfig {
fn default() -> Self {
Self {
tcp_listen_port: None,
tcp_connect_address: None,
enable_mdns: true,
enable_bluetooth: false,
enable_websocket: false,
custom: HashMap::new(),
}
}
}
pub struct SyncSubscription {
collection: String,
_handle: Box<dyn std::any::Any + Send + Sync>,
}
impl SyncSubscription {
pub fn new(collection: impl Into<String>, handle: impl std::any::Any + Send + Sync) -> Self {
eprintln!("SyncSubscription::new() - Creating subscription wrapper");
Self {
collection: collection.into(),
_handle: Box::new(handle),
}
}
pub fn collection(&self) -> &str {
&self.collection
}
}
impl Drop for SyncSubscription {
fn drop(&mut self) {
eprintln!(
"SyncSubscription::drop() - Subscription for '{}' is being dropped!",
self.collection
);
}
}
impl std::fmt::Debug for SyncSubscription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SyncSubscription")
.field("collection", &self.collection)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct Subscription {
pub collection: String,
pub query: Query,
pub qos: SubscriptionQoS,
}
impl Subscription {
pub fn all(collection: impl Into<String>) -> Self {
Self {
collection: collection.into(),
query: Query::All,
qos: SubscriptionQoS::default(),
}
}
pub fn with_query(collection: impl Into<String>, query: Query) -> Self {
Self {
collection: collection.into(),
query,
qos: SubscriptionQoS::default(),
}
}
pub fn with_qos(collection: impl Into<String>, query: Query, qos: SubscriptionQoS) -> Self {
Self {
collection: collection.into(),
query,
qos,
}
}
pub fn within_radius(
collection: impl Into<String>,
center: GeoPoint,
radius_meters: f64,
) -> Self {
Self {
collection: collection.into(),
query: Query::WithinRadius {
center,
radius_meters,
lat_field: None,
lon_field: None,
},
qos: SubscriptionQoS::default(),
}
}
pub fn within_bounds(collection: impl Into<String>, min: GeoPoint, max: GeoPoint) -> Self {
Self {
collection: collection.into(),
query: Query::WithinBounds {
min,
max,
lat_field: None,
lon_field: None,
},
qos: SubscriptionQoS::default(),
}
}
pub fn with_sync_mode(mut self, sync_mode: crate::qos::SyncMode) -> Self {
self.qos.sync_mode = sync_mode;
self
}
pub fn update_query(&mut self, query: Query) {
self.query = query;
}
pub fn update_qos(&mut self, qos: SubscriptionQoS) {
self.qos = qos;
}
pub fn update_sync_mode(&mut self, sync_mode: crate::qos::SyncMode) {
self.qos.sync_mode = sync_mode;
}
pub fn update_center(&mut self, new_center: GeoPoint) {
if let Query::WithinRadius { center, .. } = &mut self.query {
*center = new_center;
}
}
pub fn update_radius(&mut self, new_radius: f64) {
if let Query::WithinRadius { radius_meters, .. } = &mut self.query {
*radius_meters = new_radius;
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SubscriptionQoS {
pub sync_mode: crate::qos::SyncMode,
pub max_documents: Option<usize>,
pub update_rate_ms: Option<u64>,
}
impl SubscriptionQoS {
pub fn latest_only() -> Self {
Self {
sync_mode: crate::qos::SyncMode::LatestOnly,
..Default::default()
}
}
pub fn full_history() -> Self {
Self {
sync_mode: crate::qos::SyncMode::FullHistory,
..Default::default()
}
}
pub fn windowed(window_seconds: u64) -> Self {
Self {
sync_mode: crate::qos::SyncMode::WindowedHistory { window_seconds },
..Default::default()
}
}
pub fn with_max_documents(mut self, max: usize) -> Self {
self.max_documents = Some(max);
self
}
pub fn with_rate_limit(mut self, rate_ms: u64) -> Self {
self.update_rate_ms = Some(rate_ms);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Priority {
Critical = 0,
High = 1,
#[default]
Medium = 2,
Low = 3,
}
#[derive(Debug, Clone, Default)]
pub struct SyncModeMetrics {
pub total_syncs: u64,
pub full_history_syncs: u64,
pub latest_only_syncs: u64,
pub windowed_syncs: u64,
pub full_history_bytes: u64,
pub latest_only_bytes: u64,
pub windowed_bytes: u64,
pub full_history_duration_ms: u64,
pub latest_only_duration_ms: u64,
pub windowed_duration_ms: u64,
}
impl SyncModeMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn record_sync(
&mut self,
_collection: &str,
mode: crate::qos::SyncMode,
bytes: u64,
duration: std::time::Duration,
) {
self.total_syncs += 1;
let duration_ms = duration.as_millis() as u64;
match mode {
crate::qos::SyncMode::FullHistory => {
self.full_history_syncs += 1;
self.full_history_bytes += bytes;
self.full_history_duration_ms += duration_ms;
}
crate::qos::SyncMode::LatestOnly => {
self.latest_only_syncs += 1;
self.latest_only_bytes += bytes;
self.latest_only_duration_ms += duration_ms;
}
crate::qos::SyncMode::WindowedHistory { .. } => {
self.windowed_syncs += 1;
self.windowed_bytes += bytes;
self.windowed_duration_ms += duration_ms;
}
}
}
pub fn avg_full_history_bytes(&self) -> f64 {
if self.full_history_syncs == 0 {
0.0
} else {
self.full_history_bytes as f64 / self.full_history_syncs as f64
}
}
pub fn avg_latest_only_bytes(&self) -> f64 {
if self.latest_only_syncs == 0 {
0.0
} else {
self.latest_only_bytes as f64 / self.latest_only_syncs as f64
}
}
pub fn bandwidth_savings_ratio(&self) -> Option<f64> {
let fh_avg = self.avg_full_history_bytes();
let lo_avg = self.avg_latest_only_bytes();
if lo_avg == 0.0 || fh_avg == 0.0 {
None
} else {
Some(fh_avg / lo_avg)
}
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_document_creation() {
let mut fields = HashMap::new();
fields.insert("name".to_string(), Value::String("test".to_string()));
let doc = Document::new(fields.clone());
assert!(doc.id.is_none());
assert_eq!(doc.get("name"), Some(&Value::String("test".to_string())));
let doc_with_id = Document::with_id("doc1", fields);
assert_eq!(doc_with_id.id, Some("doc1".to_string()));
}
#[test]
fn test_document_field_access() {
let mut doc = Document::new(HashMap::new());
doc.set("key", Value::String("value".to_string()));
assert_eq!(doc.get("key"), Some(&Value::String("value".to_string())));
assert_eq!(doc.get("missing"), None);
}
#[test]
fn test_priority_ordering() {
assert!(Priority::Critical < Priority::High);
assert!(Priority::High < Priority::Medium);
assert!(Priority::Medium < Priority::Low);
}
#[test]
fn test_geopoint_creation() {
let point = GeoPoint::new(37.7749, -122.4194); assert_eq!(point.lat, 37.7749);
assert_eq!(point.lon, -122.4194);
}
#[test]
fn test_haversine_distance_same_point() {
let sf = GeoPoint::new(37.7749, -122.4194);
let distance = sf.distance_to(&sf);
assert!(
distance < 1.0,
"Distance to self should be ~0, got {}",
distance
);
}
#[test]
fn test_haversine_distance_known_values() {
let sf = GeoPoint::new(37.7749, -122.4194);
let la = GeoPoint::new(34.0522, -118.2437);
let distance = sf.distance_to(&la);
let expected = 559_000.0;
let tolerance = expected * 0.01;
assert!(
(distance - expected).abs() < tolerance,
"SF to LA should be ~559km, got {}m",
distance
);
}
#[test]
fn test_haversine_distance_across_equator() {
let quito = GeoPoint::new(-0.1807, -78.4678);
let buenos_aires = GeoPoint::new(-34.6037, -58.3816);
let distance = quito.distance_to(&buenos_aires);
assert!(
distance > 4_300_000.0 && distance < 4_500_000.0,
"Quito to Buenos Aires should be ~4,360km, got {}m",
distance
);
}
#[test]
fn test_geopoint_within_bounds() {
let point = GeoPoint::new(37.7749, -122.4194); let min = GeoPoint::new(37.0, -123.0);
let max = GeoPoint::new(38.0, -122.0);
assert!(point.within_bounds(&min, &max));
let outside = GeoPoint::new(40.0, -122.0);
assert!(!outside.within_bounds(&min, &max));
}
#[test]
fn test_geopoint_within_radius() {
let center = GeoPoint::new(37.7749, -122.4194);
let nearby = GeoPoint::new(37.7839, -122.4194); assert!(nearby.within_radius(¢er, 2000.0)); assert!(!nearby.within_radius(¢er, 500.0));
let la = GeoPoint::new(34.0522, -118.2437);
assert!(!la.within_radius(¢er, 100_000.0)); assert!(la.within_radius(¢er, 600_000.0)); }
#[test]
fn test_spatial_query_within_radius() {
let query = Query::WithinRadius {
center: GeoPoint::new(37.7749, -122.4194),
radius_meters: 5000.0,
lat_field: None,
lon_field: None,
};
match query {
Query::WithinRadius {
center,
radius_meters,
..
} => {
assert_eq!(center.lat, 37.7749);
assert_eq!(radius_meters, 5000.0);
}
_ => panic!("Expected WithinRadius query"),
}
}
#[test]
fn test_spatial_query_within_bounds() {
let query = Query::WithinBounds {
min: GeoPoint::new(37.0, -123.0),
max: GeoPoint::new(38.0, -122.0),
lat_field: Some("latitude".to_string()),
lon_field: Some("longitude".to_string()),
};
match query {
Query::WithinBounds {
min,
max,
lat_field,
lon_field,
} => {
assert_eq!(min.lat, 37.0);
assert_eq!(max.lon, -122.0);
assert_eq!(lat_field, Some("latitude".to_string()));
assert_eq!(lon_field, Some("longitude".to_string()));
}
_ => panic!("Expected WithinBounds query"),
}
}
#[test]
fn test_geopoint_serialization() {
let point = GeoPoint::new(37.7749, -122.4194);
let json = serde_json::to_string(&point).unwrap();
let deserialized: GeoPoint = serde_json::from_str(&json).unwrap();
assert_eq!(point, deserialized);
}
#[test]
fn test_subscription_all() {
let sub = Subscription::all("beacons");
assert_eq!(sub.collection, "beacons");
assert!(matches!(sub.query, Query::All));
}
#[test]
fn test_subscription_with_query() {
let query = Query::Eq {
field: "type".to_string(),
value: Value::String("soldier".to_string()),
};
let sub = Subscription::with_query("platforms", query);
assert_eq!(sub.collection, "platforms");
}
#[test]
fn test_subscription_within_radius() {
let center = GeoPoint::new(37.7749, -122.4194);
let sub = Subscription::within_radius("beacons", center, 5000.0);
assert_eq!(sub.collection, "beacons");
match sub.query {
Query::WithinRadius {
center: c,
radius_meters,
..
} => {
assert_eq!(c.lat, 37.7749);
assert_eq!(radius_meters, 5000.0);
}
_ => panic!("Expected WithinRadius query"),
}
}
#[test]
fn test_subscription_within_bounds() {
let min = GeoPoint::new(37.0, -123.0);
let max = GeoPoint::new(38.0, -122.0);
let sub = Subscription::within_bounds("beacons", min, max);
assert_eq!(sub.collection, "beacons");
match sub.query {
Query::WithinBounds {
min: m, max: mx, ..
} => {
assert_eq!(m.lat, 37.0);
assert_eq!(mx.lon, -122.0);
}
_ => panic!("Expected WithinBounds query"),
}
}
#[test]
fn test_subscription_with_sync_mode() {
let sub = Subscription::all("beacons").with_sync_mode(crate::qos::SyncMode::LatestOnly);
assert!(sub.qos.sync_mode.is_latest_only());
}
#[test]
fn test_subscription_qos_defaults() {
let qos = SubscriptionQoS::default();
assert!(qos.sync_mode.is_full_history());
assert!(qos.max_documents.is_none());
assert!(qos.update_rate_ms.is_none());
}
#[test]
fn test_subscription_qos_latest_only() {
let qos = SubscriptionQoS::latest_only();
assert!(qos.sync_mode.is_latest_only());
}
#[test]
fn test_subscription_qos_windowed() {
let qos = SubscriptionQoS::windowed(300);
assert!(qos.sync_mode.is_windowed());
assert_eq!(qos.sync_mode.window_seconds(), Some(300));
}
#[test]
fn test_subscription_qos_with_limits() {
let qos = SubscriptionQoS::latest_only()
.with_max_documents(100)
.with_rate_limit(1000);
assert_eq!(qos.max_documents, Some(100));
assert_eq!(qos.update_rate_ms, Some(1000));
}
#[test]
fn test_query_not() {
let inner = Query::Eq {
field: "type".to_string(),
value: Value::String("hidden".to_string()),
};
let not_query = Query::Not(Box::new(inner));
match not_query {
Query::Not(inner) => match inner.as_ref() {
Query::Eq { field, value } => {
assert_eq!(field, "type");
assert_eq!(value, &Value::String("hidden".to_string()));
}
_ => panic!("Expected Eq query inside Not"),
},
_ => panic!("Expected Not query"),
}
}
#[test]
fn test_compound_query_not_and() {
let and_query = Query::And(vec![
Query::Eq {
field: "type".to_string(),
value: Value::String("hidden".to_string()),
},
Query::Eq {
field: "status".to_string(),
value: Value::String("deleted".to_string()),
},
]);
let not_and = Query::Not(Box::new(and_query));
match not_and {
Query::Not(inner) => match inner.as_ref() {
Query::And(queries) => {
assert_eq!(queries.len(), 2);
}
_ => panic!("Expected And query inside Not"),
},
_ => panic!("Expected Not query"),
}
}
#[test]
fn test_subscription_update_query() {
let mut sub = Subscription::all("beacons");
sub.update_query(Query::WithinRadius {
center: GeoPoint::new(37.7749, -122.4194),
radius_meters: 5000.0,
lat_field: None,
lon_field: None,
});
match &sub.query {
Query::WithinRadius { radius_meters, .. } => {
assert_eq!(*radius_meters, 5000.0);
}
_ => panic!("Expected WithinRadius query"),
}
}
#[test]
fn test_subscription_update_qos() {
let mut sub = Subscription::all("beacons");
assert!(sub.qos.sync_mode.is_full_history());
sub.update_qos(SubscriptionQoS::latest_only().with_max_documents(50));
assert!(sub.qos.sync_mode.is_latest_only());
assert_eq!(sub.qos.max_documents, Some(50));
}
#[test]
fn test_subscription_update_sync_mode() {
let mut sub = Subscription::all("beacons");
sub.update_sync_mode(crate::qos::SyncMode::LatestOnly);
assert!(sub.qos.sync_mode.is_latest_only());
}
#[test]
fn test_subscription_update_center() {
let mut sub =
Subscription::within_radius("beacons", GeoPoint::new(37.7749, -122.4194), 5000.0);
sub.update_center(GeoPoint::new(34.0522, -118.2437));
match &sub.query {
Query::WithinRadius { center, .. } => {
assert_eq!(center.lat, 34.0522);
assert_eq!(center.lon, -118.2437);
}
_ => panic!("Expected WithinRadius query"),
}
}
#[test]
fn test_subscription_update_radius() {
let mut sub =
Subscription::within_radius("beacons", GeoPoint::new(37.7749, -122.4194), 5000.0);
sub.update_radius(10000.0);
match &sub.query {
Query::WithinRadius { radius_meters, .. } => {
assert_eq!(*radius_meters, 10000.0);
}
_ => panic!("Expected WithinRadius query"),
}
}
#[test]
fn test_subscription_update_center_noop_on_non_radius() {
let mut sub = Subscription::all("beacons");
sub.update_center(GeoPoint::new(34.0522, -118.2437));
assert!(matches!(sub.query, Query::All));
}
#[test]
fn test_sync_mode_metrics_new() {
let metrics = SyncModeMetrics::new();
assert_eq!(metrics.total_syncs, 0);
assert_eq!(metrics.full_history_syncs, 0);
assert_eq!(metrics.latest_only_syncs, 0);
}
#[test]
fn test_sync_mode_metrics_record_full_history() {
let mut metrics = SyncModeMetrics::new();
metrics.record_sync(
"beacons",
crate::qos::SyncMode::FullHistory,
10000,
std::time::Duration::from_millis(50),
);
assert_eq!(metrics.total_syncs, 1);
assert_eq!(metrics.full_history_syncs, 1);
assert_eq!(metrics.full_history_bytes, 10000);
assert_eq!(metrics.full_history_duration_ms, 50);
}
#[test]
fn test_sync_mode_metrics_record_latest_only() {
let mut metrics = SyncModeMetrics::new();
metrics.record_sync(
"beacons",
crate::qos::SyncMode::LatestOnly,
500,
std::time::Duration::from_millis(5),
);
assert_eq!(metrics.total_syncs, 1);
assert_eq!(metrics.latest_only_syncs, 1);
assert_eq!(metrics.latest_only_bytes, 500);
assert_eq!(metrics.latest_only_duration_ms, 5);
}
#[test]
fn test_sync_mode_metrics_bandwidth_savings() {
let mut metrics = SyncModeMetrics::new();
metrics.record_sync(
"beacons",
crate::qos::SyncMode::FullHistory,
30000,
std::time::Duration::from_millis(100),
);
metrics.record_sync(
"beacons",
crate::qos::SyncMode::LatestOnly,
100,
std::time::Duration::from_millis(2),
);
assert_eq!(metrics.avg_full_history_bytes(), 30000.0);
assert_eq!(metrics.avg_latest_only_bytes(), 100.0);
let ratio = metrics.bandwidth_savings_ratio().unwrap();
assert_eq!(ratio, 300.0);
}
#[test]
fn test_sync_mode_metrics_reset() {
let mut metrics = SyncModeMetrics::new();
metrics.record_sync(
"beacons",
crate::qos::SyncMode::LatestOnly,
500,
std::time::Duration::from_millis(5),
);
assert_eq!(metrics.total_syncs, 1);
metrics.reset();
assert_eq!(metrics.total_syncs, 0);
assert_eq!(metrics.latest_only_syncs, 0);
}
#[test]
fn test_sync_mode_metrics_windowed() {
let mut metrics = SyncModeMetrics::new();
metrics.record_sync(
"track_history",
crate::qos::SyncMode::WindowedHistory {
window_seconds: 300,
},
5000,
std::time::Duration::from_millis(20),
);
assert_eq!(metrics.total_syncs, 1);
assert_eq!(metrics.windowed_syncs, 1);
assert_eq!(metrics.windowed_bytes, 5000);
}
#[test]
fn test_query_include_deleted() {
let inner = Query::All;
let query = Query::IncludeDeleted(Box::new(inner));
assert!(query.includes_deleted());
assert!(!query.is_deleted_only());
match query.inner_query() {
Query::All => {}
_ => panic!("Expected All query inside IncludeDeleted"),
}
}
#[test]
fn test_query_deleted_only() {
let query = Query::DeletedOnly;
assert!(query.includes_deleted());
assert!(query.is_deleted_only());
}
#[test]
fn test_query_with_deleted() {
let query = Query::All;
let wrapped = query.with_deleted();
assert!(matches!(wrapped, Query::IncludeDeleted(_)));
let already_wrapped = Query::IncludeDeleted(Box::new(Query::All));
let still_wrapped = already_wrapped.with_deleted();
assert!(matches!(still_wrapped, Query::IncludeDeleted(_)));
let deleted_only = Query::DeletedOnly;
let still_deleted_only = deleted_only.with_deleted();
assert!(matches!(still_deleted_only, Query::DeletedOnly));
}
#[test]
fn test_query_matches_deletion_state_normal() {
let query = Query::All;
let mut non_deleted = Document::new(HashMap::new());
non_deleted.set("name", Value::String("test".to_string()));
assert!(query.matches_deletion_state(&non_deleted));
let mut deleted = Document::new(HashMap::new());
deleted.set("name", Value::String("test".to_string()));
deleted.set("_deleted", Value::Bool(true));
assert!(!query.matches_deletion_state(&deleted));
let mut not_deleted = Document::new(HashMap::new());
not_deleted.set("_deleted", Value::Bool(false));
assert!(query.matches_deletion_state(¬_deleted));
}
#[test]
fn test_query_matches_deletion_state_include_deleted() {
let query = Query::IncludeDeleted(Box::new(Query::All));
let non_deleted = Document::new(HashMap::new());
assert!(query.matches_deletion_state(&non_deleted));
let mut deleted = Document::new(HashMap::new());
deleted.set("_deleted", Value::Bool(true));
assert!(query.matches_deletion_state(&deleted));
}
#[test]
fn test_query_matches_deletion_state_deleted_only() {
let query = Query::DeletedOnly;
let non_deleted = Document::new(HashMap::new());
assert!(!query.matches_deletion_state(&non_deleted));
let mut deleted = Document::new(HashMap::new());
deleted.set("_deleted", Value::Bool(true));
assert!(query.matches_deletion_state(&deleted));
let mut not_deleted = Document::new(HashMap::new());
not_deleted.set("_deleted", Value::Bool(false));
assert!(!query.matches_deletion_state(¬_deleted));
}
#[test]
fn test_query_include_deleted_with_filter() {
let inner = Query::Eq {
field: "type".to_string(),
value: Value::String("contact_report".to_string()),
};
let query = Query::IncludeDeleted(Box::new(inner));
assert!(query.includes_deleted());
match query.inner_query() {
Query::Eq { field, value } => {
assert_eq!(field, "type");
assert_eq!(value, &Value::String("contact_report".to_string()));
}
_ => panic!("Expected Eq query inside IncludeDeleted"),
}
}
#[test]
fn test_query_normal_excludes_deleted() {
let queries = vec![
Query::All,
Query::Eq {
field: "x".to_string(),
value: Value::Null,
},
Query::And(vec![Query::All]),
Query::Or(vec![Query::All]),
Query::Not(Box::new(Query::All)),
];
let mut deleted_doc = Document::new(HashMap::new());
deleted_doc.set("_deleted", Value::Bool(true));
for query in queries {
assert!(
!query.matches_deletion_state(&deleted_doc),
"Query {:?} should exclude deleted docs",
query
);
}
}
}