use crate::velesql::{Condition, JoinClause};
use std::collections::HashSet;
#[derive(Debug, Clone, Default)]
#[allow(clippy::struct_field_names)]
pub struct PushdownAnalysis {
pub column_store_filters: Vec<Condition>,
pub graph_filters: Vec<Condition>,
pub post_join_filters: Vec<Condition>,
}
impl PushdownAnalysis {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_pushdown(&self) -> bool {
!self.column_store_filters.is_empty()
}
#[must_use]
pub fn total_conditions(&self) -> usize {
self.column_store_filters.len() + self.graph_filters.len() + self.post_join_filters.len()
}
}
#[must_use]
pub fn analyze_for_pushdown(
condition: &Condition,
graph_vars: &HashSet<String>,
join_tables: &HashSet<String>,
) -> PushdownAnalysis {
let mut analysis = PushdownAnalysis::new();
classify_condition(condition, graph_vars, join_tables, &mut analysis);
analysis
}
#[must_use]
pub fn extract_join_tables(joins: &[JoinClause]) -> HashSet<String> {
let mut tables = HashSet::new();
for join in joins {
tables.insert(join.table.clone());
if let Some(ref alias) = join.alias {
tables.insert(alias.clone());
}
}
tables
}
fn route_to_bucket(condition: &Condition, source: Source, analysis: &mut PushdownAnalysis) {
match source {
Source::ColumnStore => analysis.column_store_filters.push(condition.clone()),
Source::Graph => analysis.graph_filters.push(condition.clone()),
Source::Mixed | Source::Unknown => analysis.post_join_filters.push(condition.clone()),
}
}
fn classify_condition(
condition: &Condition,
graph_vars: &HashSet<String>,
join_tables: &HashSet<String>,
analysis: &mut PushdownAnalysis,
) {
match condition {
Condition::And(left, right) => {
classify_condition(left, graph_vars, join_tables, analysis);
classify_condition(right, graph_vars, join_tables, analysis);
}
Condition::Or(left, right) => {
let combined = combine_sources(
get_condition_source(left, graph_vars, join_tables),
get_condition_source(right, graph_vars, join_tables),
);
route_to_bucket(condition, combined, analysis);
}
Condition::Not(inner) => {
let source = get_condition_source(inner, graph_vars, join_tables);
route_to_bucket(condition, source, analysis);
}
Condition::Group(inner) => {
classify_condition(inner, graph_vars, join_tables, analysis);
}
_ => {
let source = get_condition_source(condition, graph_vars, join_tables);
route_to_bucket(condition, source, analysis);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Source {
ColumnStore,
Graph,
Mixed,
Unknown,
}
fn get_condition_source(
condition: &Condition,
graph_vars: &HashSet<String>,
join_tables: &HashSet<String>,
) -> Source {
match condition {
Condition::Comparison(cmp) => classify_column(&cmp.column, graph_vars, join_tables),
Condition::In(inc) => classify_column(&inc.column, graph_vars, join_tables),
Condition::Between(btw) => classify_column(&btw.column, graph_vars, join_tables),
Condition::Like(like) => classify_column(&like.column, graph_vars, join_tables),
Condition::IsNull(is_null) => classify_column(&is_null.column, graph_vars, join_tables),
Condition::Match(m) => {
classify_column(&m.column, graph_vars, join_tables)
}
Condition::Contains(cc) => classify_column(&cc.column, graph_vars, join_tables),
Condition::ContainsText(ct) => classify_column(&ct.column, graph_vars, join_tables),
Condition::GeoDistance(gd) => classify_column(&gd.column, graph_vars, join_tables),
Condition::GeoBbox(gb) => classify_column(&gb.column, graph_vars, join_tables),
Condition::GraphMatch(_)
| Condition::VectorSearch(_)
| Condition::VectorFusedSearch(_)
| Condition::SparseVectorSearch(_)
| Condition::Similarity(_) => Source::Graph,
Condition::And(left, right) | Condition::Or(left, right) => {
let left_source = get_condition_source(left, graph_vars, join_tables);
let right_source = get_condition_source(right, graph_vars, join_tables);
combine_sources(left_source, right_source)
}
Condition::Not(inner) | Condition::Group(inner) => {
get_condition_source(inner, graph_vars, join_tables)
}
}
}
fn classify_column(
column: &str,
graph_vars: &HashSet<String>,
join_tables: &HashSet<String>,
) -> Source {
if let Some((table, _column)) = column.split_once('.') {
if join_tables.contains(table) {
return Source::ColumnStore;
}
if graph_vars.contains(table) {
return Source::Graph;
}
return Source::Unknown;
}
Source::Graph
}
fn combine_sources(a: Source, b: Source) -> Source {
match (a, b) {
(Source::ColumnStore, Source::ColumnStore) => Source::ColumnStore,
(Source::Graph, Source::Graph) => Source::Graph,
(Source::Unknown, _) | (_, Source::Unknown) => Source::Unknown,
_ => Source::Mixed,
}
}