use std::collections::HashSet;
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterValue {
String(String),
Int64(i64),
Uint64(u64),
Float64(f64),
Bool(bool),
Null,
}
impl FilterValue {
pub fn eq_match(&self, other: &FilterValue) -> bool {
match (self, other) {
(FilterValue::String(a), FilterValue::String(b)) => a == b,
(FilterValue::Int64(a), FilterValue::Int64(b)) => a == b,
(FilterValue::Uint64(a), FilterValue::Uint64(b)) => a == b,
(FilterValue::Float64(a), FilterValue::Float64(b)) => {
(a - b).abs() < f64::EPSILON
}
(FilterValue::Bool(a), FilterValue::Bool(b)) => a == b,
(FilterValue::Null, FilterValue::Null) => true,
_ => false,
}
}
pub fn partial_cmp(&self, other: &FilterValue) -> Option<std::cmp::Ordering> {
match (self, other) {
(FilterValue::Int64(a), FilterValue::Int64(b)) => Some(a.cmp(b)),
(FilterValue::Uint64(a), FilterValue::Uint64(b)) => Some(a.cmp(b)),
(FilterValue::Float64(a), FilterValue::Float64(b)) => a.partial_cmp(b),
(FilterValue::String(a), FilterValue::String(b)) => Some(a.cmp(b)),
_ => None,
}
}
}
impl fmt::Display for FilterValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FilterValue::String(s) => write!(f, "'{}'", s),
FilterValue::Int64(i) => write!(f, "{}", i),
FilterValue::Uint64(u) => write!(f, "{}u64", u),
FilterValue::Float64(v) => write!(f, "{}", v),
FilterValue::Bool(b) => write!(f, "{}", b),
FilterValue::Null => write!(f, "NULL"),
}
}
}
impl From<&str> for FilterValue {
fn from(s: &str) -> Self {
FilterValue::String(s.to_string())
}
}
impl From<String> for FilterValue {
fn from(s: String) -> Self {
FilterValue::String(s)
}
}
impl From<i64> for FilterValue {
fn from(i: i64) -> Self {
FilterValue::Int64(i)
}
}
impl From<u64> for FilterValue {
fn from(u: u64) -> Self {
FilterValue::Uint64(u)
}
}
impl From<f64> for FilterValue {
fn from(f: f64) -> Self {
FilterValue::Float64(f)
}
}
impl From<bool> for FilterValue {
fn from(b: bool) -> Self {
FilterValue::Bool(b)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterAtom {
Eq {
field: String,
value: FilterValue,
},
Ne {
field: String,
value: FilterValue,
},
In {
field: String,
values: Vec<FilterValue>,
},
NotIn {
field: String,
values: Vec<FilterValue>,
},
Range {
field: String,
min: Option<FilterValue>,
max: Option<FilterValue>,
min_inclusive: bool,
max_inclusive: bool,
},
Prefix {
field: String,
prefix: String,
},
Contains {
field: String,
substring: String,
},
HasTag {
tag: String,
},
True,
False,
}
impl FilterAtom {
pub fn eq(field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
FilterAtom::Eq {
field: field.into(),
value: value.into(),
}
}
pub fn in_set(field: impl Into<String>, values: Vec<FilterValue>) -> Self {
FilterAtom::In {
field: field.into(),
values,
}
}
pub fn range(
field: impl Into<String>,
min: Option<FilterValue>,
max: Option<FilterValue>,
) -> Self {
FilterAtom::Range {
field: field.into(),
min,
max,
min_inclusive: true,
max_inclusive: true,
}
}
pub fn range_exclusive(
field: impl Into<String>,
min: Option<FilterValue>,
max: Option<FilterValue>,
) -> Self {
FilterAtom::Range {
field: field.into(),
min,
max,
min_inclusive: false,
max_inclusive: false,
}
}
pub fn field(&self) -> Option<&str> {
match self {
FilterAtom::Eq { field, .. } => Some(field),
FilterAtom::Ne { field, .. } => Some(field),
FilterAtom::In { field, .. } => Some(field),
FilterAtom::NotIn { field, .. } => Some(field),
FilterAtom::Range { field, .. } => Some(field),
FilterAtom::Prefix { field, .. } => Some(field),
FilterAtom::Contains { field, .. } => Some(field),
FilterAtom::HasTag { .. } => None,
FilterAtom::True | FilterAtom::False => None,
}
}
pub fn is_trivially_true(&self) -> bool {
matches!(self, FilterAtom::True)
}
pub fn is_trivially_false(&self) -> bool {
matches!(self, FilterAtom::False)
}
pub fn negate(&self) -> FilterAtom {
match self {
FilterAtom::Eq { field, value } => FilterAtom::Ne {
field: field.clone(),
value: value.clone(),
},
FilterAtom::Ne { field, value } => FilterAtom::Eq {
field: field.clone(),
value: value.clone(),
},
FilterAtom::In { field, values } => FilterAtom::NotIn {
field: field.clone(),
values: values.clone(),
},
FilterAtom::NotIn { field, values } => FilterAtom::In {
field: field.clone(),
values: values.clone(),
},
FilterAtom::True => FilterAtom::False,
FilterAtom::False => FilterAtom::True,
other => other.clone(), }
}
}
impl fmt::Display for FilterAtom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FilterAtom::Eq { field, value } => write!(f, "{} = {}", field, value),
FilterAtom::Ne { field, value } => write!(f, "{} != {}", field, value),
FilterAtom::In { field, values } => {
let vals: Vec<_> = values.iter().map(|v| v.to_string()).collect();
write!(f, "{} IN ({})", field, vals.join(", "))
}
FilterAtom::NotIn { field, values } => {
let vals: Vec<_> = values.iter().map(|v| v.to_string()).collect();
write!(f, "{} NOT IN ({})", field, vals.join(", "))
}
FilterAtom::Range { field, min, max, min_inclusive, max_inclusive } => {
let left = if *min_inclusive { "[" } else { "(" };
let right = if *max_inclusive { "]" } else { ")" };
let min_str = min.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-∞".to_string());
let max_str = max.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "∞".to_string());
write!(f, "{} ∈ {}{}, {}{}", field, left, min_str, max_str, right)
}
FilterAtom::Prefix { field, prefix } => write!(f, "{} STARTS WITH '{}'", field, prefix),
FilterAtom::Contains { field, substring } => write!(f, "{} CONTAINS '{}'", field, substring),
FilterAtom::HasTag { tag } => write!(f, "HAS_TAG('{}')", tag),
FilterAtom::True => write!(f, "TRUE"),
FilterAtom::False => write!(f, "FALSE"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Disjunction {
pub atoms: Vec<FilterAtom>,
}
impl Disjunction {
pub fn new(atoms: Vec<FilterAtom>) -> Self {
Self { atoms }
}
pub fn single(atom: FilterAtom) -> Self {
Self { atoms: vec![atom] }
}
pub fn is_trivially_true(&self) -> bool {
self.atoms.iter().any(|a| a.is_trivially_true())
}
pub fn is_trivially_false(&self) -> bool {
self.atoms.is_empty() || self.atoms.iter().all(|a| a.is_trivially_false())
}
pub fn simplify(self) -> Self {
let atoms: Vec<_> = self.atoms.into_iter()
.filter(|a| !a.is_trivially_false())
.collect();
if atoms.iter().any(|a| a.is_trivially_true()) {
return Self { atoms: vec![FilterAtom::True] };
}
if atoms.is_empty() {
return Self { atoms: vec![FilterAtom::False] };
}
Self { atoms }
}
}
impl fmt::Display for Disjunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.atoms.len() == 1 {
write!(f, "{}", self.atoms[0])
} else {
let parts: Vec<_> = self.atoms.iter().map(|a| a.to_string()).collect();
write!(f, "({})", parts.join(" OR "))
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FilterIR {
pub clauses: Vec<Disjunction>,
}
impl FilterIR {
pub fn all() -> Self {
Self { clauses: vec![] }
}
pub fn none() -> Self {
Self {
clauses: vec![Disjunction::single(FilterAtom::False)],
}
}
pub fn from_atom(atom: FilterAtom) -> Self {
Self {
clauses: vec![Disjunction::single(atom)],
}
}
pub fn from_disjunction(disj: Disjunction) -> Self {
Self { clauses: vec![disj] }
}
pub fn and(mut self, other: FilterIR) -> Self {
self.clauses.extend(other.clauses);
self
}
pub fn and_atom(mut self, atom: FilterAtom) -> Self {
self.clauses.push(Disjunction::single(atom));
self
}
pub fn or(self, other: FilterIR) -> Self {
if self.clauses.is_empty() {
return other;
}
if other.clauses.is_empty() {
return self;
}
let mut new_clauses = Vec::new();
for c1 in &self.clauses {
for c2 in &other.clauses {
let mut combined = c1.atoms.clone();
combined.extend(c2.atoms.clone());
new_clauses.push(Disjunction::new(combined));
}
}
FilterIR { clauses: new_clauses }
}
pub fn is_all(&self) -> bool {
self.clauses.is_empty() || self.clauses.iter().all(|c| c.is_trivially_true())
}
pub fn is_none(&self) -> bool {
self.clauses.iter().any(|c| c.is_trivially_false())
}
pub fn simplify(self) -> Self {
let clauses: Vec<_> = self.clauses
.into_iter()
.map(|c| c.simplify())
.filter(|c| !c.is_trivially_true())
.collect();
if clauses.iter().any(|c| c.is_trivially_false()) {
return Self::none();
}
Self { clauses }
}
pub fn atoms_for_field(&self, field: &str) -> Vec<&FilterAtom> {
self.clauses
.iter()
.flat_map(|c| c.atoms.iter())
.filter(|a| a.field() == Some(field))
.collect()
}
pub fn constrains_field(&self, field: &str) -> bool {
!self.atoms_for_field(field).is_empty()
}
pub fn constrained_fields(&self) -> HashSet<&str> {
self.clauses
.iter()
.flat_map(|c| c.atoms.iter())
.filter_map(|a| a.field())
.collect()
}
}
impl Default for FilterIR {
fn default() -> Self {
Self::all()
}
}
impl fmt::Display for FilterIR {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.clauses.is_empty() {
return write!(f, "TRUE");
}
let parts: Vec<_> = self.clauses.iter().map(|c| c.to_string()).collect();
write!(f, "{}", parts.join(" AND "))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthScope {
pub allowed_namespaces: Vec<String>,
pub tenant_id: Option<String>,
pub project_id: Option<String>,
pub expires_at: Option<u64>,
pub capabilities: AuthCapabilities,
pub acl_tags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct AuthCapabilities {
pub can_read: bool,
pub can_write: bool,
pub can_delete: bool,
pub can_admin: bool,
}
impl AuthScope {
pub fn for_namespace(namespace: impl Into<String>) -> Self {
Self {
allowed_namespaces: vec![namespace.into()],
tenant_id: None,
project_id: None,
expires_at: None,
capabilities: AuthCapabilities {
can_read: true,
can_write: false,
can_delete: false,
can_admin: false,
},
acl_tags: vec![],
}
}
pub fn full_access(namespace: impl Into<String>) -> Self {
Self {
allowed_namespaces: vec![namespace.into()],
tenant_id: None,
project_id: None,
expires_at: None,
capabilities: AuthCapabilities {
can_read: true,
can_write: true,
can_delete: true,
can_admin: false,
},
acl_tags: vec![],
}
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.allowed_namespaces.push(namespace.into());
self
}
pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
pub fn with_project(mut self, project_id: impl Into<String>) -> Self {
self.project_id = Some(project_id.into());
self
}
pub fn with_expiry(mut self, expires_at: u64) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn with_acl_tags(mut self, tags: Vec<String>) -> Self {
self.acl_tags = tags;
self
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
now > expires_at
} else {
false
}
}
pub fn is_namespace_allowed(&self, namespace: &str) -> bool {
self.allowed_namespaces.iter().any(|ns| ns == namespace)
}
pub fn to_filter_ir(&self) -> FilterIR {
let mut filter = FilterIR::all();
if self.allowed_namespaces.len() == 1 {
filter = filter.and_atom(FilterAtom::eq(
"namespace",
self.allowed_namespaces[0].clone(),
));
} else if !self.allowed_namespaces.is_empty() {
filter = filter.and_atom(FilterAtom::in_set(
"namespace",
self.allowed_namespaces
.iter()
.map(|ns| FilterValue::String(ns.clone()))
.collect(),
));
}
if let Some(ref tenant_id) = self.tenant_id {
filter = filter.and_atom(FilterAtom::eq("tenant_id", tenant_id.clone()));
}
if let Some(ref project_id) = self.project_id {
filter = filter.and_atom(FilterAtom::eq("project_id", project_id.clone()));
}
filter
}
}
pub trait FilteredExecutor {
type QueryOp;
type Result;
type Error;
fn execute(
&self,
query: &Self::QueryOp,
filter_ir: &FilterIR,
auth_scope: &AuthScope,
) -> Result<Self::Result, Self::Error>;
fn effective_filter(&self, filter_ir: &FilterIR, auth_scope: &AuthScope) -> FilterIR {
auth_scope.to_filter_ir().and(filter_ir.clone())
}
}
#[derive(Debug, Clone, Default)]
pub struct FilterBuilder {
clauses: Vec<Disjunction>,
}
impl FilterBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn eq(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::eq(field, value)));
self
}
pub fn ne(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Ne {
field: field.to_string(),
value: value.into(),
}));
self
}
pub fn in_set(mut self, field: &str, values: Vec<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::in_set(field, values)));
self
}
pub fn range(
mut self,
field: &str,
min: Option<impl Into<FilterValue>>,
max: Option<impl Into<FilterValue>>,
) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::range(
field,
min.map(Into::into),
max.map(Into::into),
)));
self
}
pub fn gt(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Range {
field: field.to_string(),
min: Some(value.into()),
max: None,
min_inclusive: false,
max_inclusive: false,
}));
self
}
pub fn gte(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Range {
field: field.to_string(),
min: Some(value.into()),
max: None,
min_inclusive: true,
max_inclusive: false,
}));
self
}
pub fn lt(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Range {
field: field.to_string(),
min: None,
max: Some(value.into()),
min_inclusive: false,
max_inclusive: false,
}));
self
}
pub fn lte(mut self, field: &str, value: impl Into<FilterValue>) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Range {
field: field.to_string(),
min: None,
max: Some(value.into()),
min_inclusive: false,
max_inclusive: true,
}));
self
}
pub fn prefix(mut self, field: &str, prefix: &str) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Prefix {
field: field.to_string(),
prefix: prefix.to_string(),
}));
self
}
pub fn contains(mut self, field: &str, substring: &str) -> Self {
self.clauses.push(Disjunction::single(FilterAtom::Contains {
field: field.to_string(),
substring: substring.to_string(),
}));
self
}
pub fn namespace(self, namespace: &str) -> Self {
self.eq("namespace", namespace)
}
pub fn doc_ids(self, doc_ids: &[u64]) -> Self {
self.in_set(
"doc_id",
doc_ids.iter().map(|&id| FilterValue::Uint64(id)).collect(),
)
}
pub fn time_range(self, field: &str, start: Option<u64>, end: Option<u64>) -> Self {
self.range(
field,
start.map(FilterValue::Uint64),
end.map(FilterValue::Uint64),
)
}
pub fn or_atoms(mut self, atoms: Vec<FilterAtom>) -> Self {
self.clauses.push(Disjunction::new(atoms));
self
}
pub fn build(self) -> FilterIR {
FilterIR { clauses: self.clauses }
}
}
#[macro_export]
macro_rules! filter_ir {
() => {
$crate::filter_ir::FilterIR::all()
};
($field:ident = $value:expr $(, $($rest:tt)*)?) => {{
let mut builder = $crate::filter_ir::FilterBuilder::new()
.eq(stringify!($field), $value);
$(
builder = filter_ir!(@chain builder, $($rest)*);
)?
builder.build()
}};
(@chain $builder:expr, $field:ident = $value:expr $(, $($rest:tt)*)?) => {{
let builder = $builder.eq(stringify!($field), $value);
$(
filter_ir!(@chain builder, $($rest)*)
)?
builder
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_atom_creation() {
let eq = FilterAtom::eq("namespace", "my_ns");
assert_eq!(eq.field(), Some("namespace"));
let range = FilterAtom::range("timestamp", Some(FilterValue::Uint64(1000)), Some(FilterValue::Uint64(2000)));
assert_eq!(range.field(), Some("timestamp"));
}
#[test]
fn test_filter_ir_conjunction() {
let filter1 = FilterIR::from_atom(FilterAtom::eq("namespace", "ns1"));
let filter2 = FilterIR::from_atom(FilterAtom::eq("project_id", "proj1"));
let combined = filter1.and(filter2);
assert_eq!(combined.clauses.len(), 2);
}
#[test]
fn test_auth_scope_to_filter() {
let scope = AuthScope::for_namespace("production")
.with_tenant("acme_corp");
let filter = scope.to_filter_ir();
assert!(filter.constrains_field("namespace"));
assert!(filter.constrains_field("tenant_id"));
assert!(!filter.constrains_field("project_id"));
}
#[test]
fn test_effective_filter() {
let auth = AuthScope::for_namespace("production");
let user_filter = FilterBuilder::new()
.eq("source", "documents")
.time_range("created_at", Some(1000), Some(2000))
.build();
let effective = auth.to_filter_ir().and(user_filter);
assert_eq!(effective.clauses.len(), 3);
assert!(effective.constrains_field("namespace"));
assert!(effective.constrains_field("source"));
assert!(effective.constrains_field("created_at"));
}
#[test]
fn test_filter_builder() {
let filter = FilterBuilder::new()
.namespace("my_namespace")
.eq("project_id", "proj_123")
.doc_ids(&[1, 2, 3, 4, 5])
.time_range("timestamp", Some(1000), None)
.build();
assert_eq!(filter.clauses.len(), 4);
}
#[test]
fn test_filter_simplification() {
let filter = FilterIR::from_atom(FilterAtom::True)
.and(FilterIR::from_atom(FilterAtom::eq("x", "y")));
let simplified = filter.simplify();
assert_eq!(simplified.clauses.len(), 1);
let filter2 = FilterIR::from_atom(FilterAtom::False)
.and(FilterIR::from_atom(FilterAtom::eq("x", "y")));
let simplified2 = filter2.simplify();
assert!(simplified2.is_none());
}
#[test]
fn test_filter_display() {
let filter = FilterBuilder::new()
.eq("namespace", "prod")
.range("timestamp", Some(1000i64), Some(2000i64))
.build();
let display = filter.to_string();
assert!(display.contains("namespace"));
assert!(display.contains("timestamp"));
}
}