use mongreldb_core::query::Condition;
use mongreldb_kit_core::query::{Expr, Literal};
use mongreldb_kit_core::schema::{ColumnType, IndexKind as KitIndexKind, Table as KitTable};
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct PushdownPlan {
pub conditions: Vec<Condition>,
pub fully_translated: bool,
}
impl PushdownPlan {
pub fn can_push(&self) -> bool {
!self.conditions.is_empty()
}
}
pub fn translate_predicate(table: &KitTable, expr: &Expr) -> Option<PushdownPlan> {
let mut conditions = Vec::new();
let fully = collect_conditions(table, expr, &mut conditions);
if conditions.is_empty() {
return None;
}
Some(PushdownPlan {
conditions,
fully_translated: fully,
})
}
fn collect_conditions(table: &KitTable, expr: &Expr, out: &mut Vec<Condition>) -> bool {
match expr {
Expr::And(parts) => {
let mut all = true;
for part in parts {
if !collect_conditions(table, part, out) {
all = false;
}
}
all
}
Expr::Eq(a, b) => push_if_some(out, try_translate_eq(table, a, b)),
Expr::Lt(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Lt)),
Expr::Lte(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Lte)),
Expr::Gt(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Gt)),
Expr::Gte(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Gte)),
Expr::In(a, list) => push_if_some(out, try_translate_in(table, a, list)),
Expr::BytesPrefix(a, prefix) => {
push_if_some(out, try_translate_bytes_prefix(table, a, prefix))
}
Expr::Contains(a, needle) => {
if let Some(c) = try_translate_contains(table, a, needle) {
out.push(c);
}
false
}
Expr::Like(a, pattern) => {
if let Some(c) = try_translate_like(table, a, pattern) {
out.push(c);
}
false
}
Expr::IsNull(a) => {
if let Some(c) = try_translate_null(table, a, true) {
out.push(c);
}
false
}
Expr::IsNotNull(a) => {
if let Some(c) = try_translate_null(table, a, false) {
out.push(c);
}
false
}
_ => false,
}
}
fn try_translate_null(table: &KitTable, a: &Expr, is_null: bool) -> Option<Condition> {
let Expr::Column(col_name) = a else {
return None;
};
let column_id = table.column(col_name)?.id as u16;
Some(if is_null {
Condition::IsNull { column_id }
} else {
Condition::IsNotNull { column_id }
})
}
fn push_if_some(out: &mut Vec<Condition>, opt: Option<Condition>) -> bool {
match opt {
Some(c) => {
out.push(c);
true
}
None => false,
}
}
#[derive(Clone, Copy)]
enum CmpOp {
Lt,
Lte,
Gt,
Gte,
}
fn extract_column_literal<'a>(a: &'a Expr, b: &'a Expr) -> Option<(&'a str, &'a Literal)> {
match (a, b) {
(Expr::Column(name), Expr::Literal(lit)) => Some((name.as_str(), lit)),
(Expr::Literal(lit), Expr::Column(name)) => Some((name.as_str(), lit)),
_ => None, }
}
fn try_translate_eq(table: &KitTable, a: &Expr, b: &Expr) -> Option<Condition> {
let (col_name, lit) = extract_column_literal(a, b)?;
let col = table.column(col_name)?;
let col_id = col.id as u16;
let ty = col.storage_type;
if table.primary_key.len() == 1 && table.primary_key[0] == col_name {
let key = literal_to_index_key(lit, ty)?;
return Some(Condition::Pk(key));
}
if has_bitmap_index(table, col_name) {
let value = literal_to_index_key(lit, ty)?;
return Some(Condition::BitmapEq {
column_id: col_id,
value,
});
}
None
}
fn try_translate_cmp(table: &KitTable, a: &Expr, b: &Expr, op: CmpOp) -> Option<Condition> {
let (col_name, lit) = extract_column_literal(a, b)?;
let col = table.column(col_name)?;
let col_id = col.id as u16;
let ty = col.storage_type;
if is_int_type(ty) {
let v = literal_to_i64(lit)?;
let (lo, hi) = match op {
CmpOp::Lt => (i64::MIN, v.saturating_sub(1)),
CmpOp::Lte => (i64::MIN, v),
CmpOp::Gt => (v.saturating_add(1), i64::MAX),
CmpOp::Gte => (v, i64::MAX),
};
Some(Condition::Range {
column_id: col_id,
lo,
hi,
})
} else if is_float_type(ty) {
let v = literal_to_f64(lit)?;
let (lo, lo_inc, hi, hi_inc) = match op {
CmpOp::Lt => (f64::NEG_INFINITY, false, v, false),
CmpOp::Lte => (f64::NEG_INFINITY, false, v, true),
CmpOp::Gt => (v, false, f64::INFINITY, false),
CmpOp::Gte => (v, true, f64::INFINITY, false),
};
Some(Condition::RangeF64 {
column_id: col_id,
lo,
lo_inclusive: lo_inc,
hi,
hi_inclusive: hi_inc,
})
} else {
None
}
}
fn has_fm_index(table: &KitTable, col_name: &str) -> bool {
table
.indexes
.iter()
.any(|idx| idx.kind == KitIndexKind::Fm && idx.columns.iter().any(|c| c == col_name))
}
fn try_translate_like(table: &KitTable, a: &Expr, pattern: &str) -> Option<Condition> {
let Expr::Column(col_name) = a else {
return None;
};
let col = table.column(col_name)?;
if !has_fm_index(table, col_name) || pattern.contains('\\') {
return None;
}
let patterns: Vec<Vec<u8>> = pattern
.split(['%', '_'])
.filter(|s| !s.is_empty())
.map(|s| s.as_bytes().to_vec())
.collect();
if patterns.is_empty() {
return None;
}
Some(Condition::FmContainsAll {
column_id: col.id as u16,
patterns,
})
}
fn try_translate_contains(table: &KitTable, a: &Expr, needle: &str) -> Option<Condition> {
let Expr::Column(col_name) = a else {
return None;
};
let col = table.column(col_name)?;
if !has_fm_index(table, col_name) {
return None;
}
Some(Condition::FmContains {
column_id: col.id as u16,
pattern: needle.as_bytes().to_vec(),
})
}
fn try_translate_bytes_prefix(table: &KitTable, a: &Expr, prefix: &str) -> Option<Condition> {
let Expr::Column(col_name) = a else {
return None;
};
let col = table.column(col_name)?;
if !has_bitmap_index(table, col_name) {
return None;
}
Some(Condition::BytesPrefix {
column_id: col.id as u16,
prefix: prefix.as_bytes().to_vec(),
})
}
fn try_translate_in(table: &KitTable, a: &Expr, list: &[Literal]) -> Option<Condition> {
let Expr::Column(col_name) = a else {
return None;
};
let col = table.column(col_name)?;
let col_id = col.id as u16;
let ty = col.storage_type;
if !has_bitmap_index(table, col_name) {
return None;
}
let mut values = Vec::with_capacity(list.len());
for lit in list {
values.push(literal_to_index_key(lit, ty)?);
}
Some(Condition::BitmapIn {
column_id: col_id,
values,
})
}
pub fn pk_conditions(table: &KitTable, pk_map: &Map<String, Value>) -> Option<Vec<Condition>> {
if pk_map.is_empty() || table.primary_key.is_empty() {
return None;
}
let mut conditions = Vec::with_capacity(table.primary_key.len());
for pk_name in &table.primary_key {
let value = pk_map.get(pk_name)?;
let col = table.column(pk_name)?;
let lit = json_to_literal(value)?;
let key = literal_to_index_key(&lit, col.storage_type)?;
if table.primary_key.len() == 1 {
conditions.push(Condition::Pk(key));
} else {
conditions.push(Condition::BitmapEq {
column_id: col.id as u16,
value: key,
});
}
}
Some(conditions)
}
fn has_bitmap_index(table: &KitTable, col_name: &str) -> bool {
table
.indexes
.iter()
.any(|idx| {
idx.kind == KitIndexKind::Bitmap && idx.columns.iter().any(|c| c == col_name)
})
|| table
.unique_constraints
.iter()
.any(|uq| uq.columns.iter().any(|c| c == col_name))
|| table.primary_key.contains(&col_name.to_string())
}
pub(crate) fn has_declared_bitmap_index(table: &KitTable, col_name: &str) -> bool {
table
.indexes
.iter()
.any(|idx| idx.columns.iter().any(|c| c == col_name))
|| table
.unique_constraints
.iter()
.any(|uq| uq.columns.iter().any(|c| c == col_name))
}
pub(crate) fn value_index_key(v: &Value, ty: ColumnType) -> Option<Vec<u8>> {
let lit = match v {
Value::Bool(b) => Literal::Bool(*b),
Value::Number(n) => n
.as_i64()
.map(Literal::Int)
.or_else(|| n.as_f64().map(Literal::Float))?,
Value::String(s) => Literal::Text(s.clone()),
_ => return None,
};
literal_to_index_key(&lit, ty)
}
fn is_int_type(ty: ColumnType) -> bool {
matches!(
ty,
ColumnType::Int8
| ColumnType::Int16
| ColumnType::Int32
| ColumnType::Int64
| ColumnType::Bool
| ColumnType::TimestampNanos
)
}
fn is_float_type(ty: ColumnType) -> bool {
matches!(ty, ColumnType::Float32 | ColumnType::Float64)
}
fn literal_to_index_key(lit: &Literal, ty: ColumnType) -> Option<Vec<u8>> {
match lit {
Literal::Null => None, Literal::Bool(b) => Some(vec![*b as u8]),
Literal::Int(n) => {
if is_float_type(ty) {
Some((*n as f64).to_bits().to_be_bytes().to_vec())
} else {
Some(n.to_be_bytes().to_vec())
}
}
Literal::Float(f) => Some(f.to_bits().to_be_bytes().to_vec()),
Literal::Text(s) => Some(s.as_bytes().to_vec()),
Literal::Json(v) => Some(serde_json::to_vec(v).ok()?),
}
}
fn literal_to_i64(lit: &Literal) -> Option<i64> {
match lit {
Literal::Int(n) => Some(*n),
Literal::Bool(b) => Some(*b as i64),
_ => None,
}
}
fn literal_to_f64(lit: &Literal) -> Option<f64> {
match lit {
Literal::Int(n) => Some(*n as f64),
Literal::Float(f) => Some(*f),
_ => None,
}
}
fn json_to_literal(value: &Value) -> Option<Literal> {
match value {
Value::Null => Some(Literal::Null),
Value::Bool(b) => Some(Literal::Bool(*b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Some(Literal::Int(i))
} else {
Some(Literal::Float(n.as_f64()?))
}
}
Value::String(s) => Some(Literal::Text(s.clone())),
Value::Array(_) | Value::Object(_) => Some(Literal::Json(value.clone())),
}
}
#[cfg(test)]
mod tests {
use super::*;
use mongreldb_kit_core::query::Expr;
use mongreldb_kit_core::schema::{Column, Index, Table as KitTable};
fn sample_table() -> KitTable {
KitTable {
id: 1,
name: "users".into(),
columns: vec![
{
let mut c = Column::new(1, "id", ColumnType::Int64);
c.primary_key = true;
c
},
Column::new(2, "email", ColumnType::Text),
{
let mut c = Column::new(3, "age", ColumnType::Int64);
c.nullable = true;
c
},
{
let mut c = Column::new(4, "score", ColumnType::Float64);
c.nullable = true;
c
},
],
primary_key: vec!["id".into()],
indexes: vec![Index {
name: "idx_email".into(),
columns: vec!["email".into()],
unique: false,
kind: Default::default(),
}],
foreign_keys: vec![],
unique_constraints: vec![],
check_constraints: vec![],
}
}
#[test]
fn translate_pk_eq() {
let t = sample_table();
let expr = Expr::Eq(
Box::new(Expr::Column("id".into())),
Box::new(Expr::Literal(Literal::Int(42))),
);
let plan = translate_predicate(&t, &expr).expect("should translate");
assert!(plan.fully_translated);
assert_eq!(plan.conditions.len(), 1);
assert!(matches!(plan.conditions[0], Condition::Pk(_)));
}
#[test]
fn translate_bitmap_eq() {
let t = sample_table();
let expr = Expr::Eq(
Box::new(Expr::Column("email".into())),
Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
);
let plan = translate_predicate(&t, &expr).expect("should translate");
assert!(plan.fully_translated);
assert!(matches!(
plan.conditions[0],
Condition::BitmapEq { column_id: 2, .. }
));
}
#[test]
fn translate_int_range() {
let t = sample_table();
let expr = Expr::Gte(
Box::new(Expr::Column("age".into())),
Box::new(Expr::Literal(Literal::Int(18))),
);
let plan = translate_predicate(&t, &expr).expect("should translate");
assert!(plan.fully_translated);
assert!(matches!(
plan.conditions[0],
Condition::Range {
column_id: 3,
lo: 18,
..
}
));
}
#[test]
fn translate_and_of_translatable() {
let t = sample_table();
let expr = Expr::And(vec![
Expr::Eq(
Box::new(Expr::Column("email".into())),
Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
),
Expr::Gt(
Box::new(Expr::Column("age".into())),
Box::new(Expr::Literal(Literal::Int(21))),
),
]);
let plan = translate_predicate(&t, &expr).expect("should translate");
assert!(plan.fully_translated);
assert_eq!(plan.conditions.len(), 2);
}
#[test]
fn translate_and_partial() {
let t = sample_table();
let expr = Expr::And(vec![
Expr::Eq(
Box::new(Expr::Column("email".into())),
Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
),
Expr::Or(vec![
Expr::Gt(
Box::new(Expr::Column("age".into())),
Box::new(Expr::Literal(Literal::Int(21))),
),
Expr::Lt(
Box::new(Expr::Column("score".into())),
Box::new(Expr::Literal(Literal::Float(5.0))),
),
]),
]);
let plan = translate_predicate(&t, &expr).expect("should partially translate");
assert!(!plan.fully_translated); assert_eq!(plan.conditions.len(), 1); }
#[test]
fn translate_unsupported_returns_none() {
let t = sample_table();
let expr = Expr::Or(vec![
Expr::Eq(
Box::new(Expr::Column("email".into())),
Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
),
Expr::Eq(
Box::new(Expr::Column("email".into())),
Box::new(Expr::Literal(Literal::Text("c@d.com".into()))),
),
]);
assert!(translate_predicate(&t, &expr).is_none());
}
#[test]
fn translate_in_to_bitmap_in() {
let t = sample_table();
let expr = Expr::In(
Box::new(Expr::Column("email".into())),
vec![
Literal::Text("a@b.com".into()),
Literal::Text("c@d.com".into()),
],
);
let plan = translate_predicate(&t, &expr).expect("should translate");
assert!(plan.fully_translated);
assert!(matches!(
plan.conditions[0],
Condition::BitmapIn { column_id: 2, .. }
));
}
#[test]
fn pk_conditions_single_col() {
let t = sample_table();
let mut pk_map = Map::new();
pk_map.insert("id".into(), Value::Number(42.into()));
let conds = pk_conditions(&t, &pk_map).expect("should build");
assert_eq!(conds.len(), 1);
assert!(matches!(conds[0], Condition::Pk(_)));
}
}