1use mongreldb_core::query::Condition;
39use mongreldb_kit_core::query::{Expr, Literal};
40use mongreldb_kit_core::schema::{ColumnType, IndexKind as KitIndexKind, Table as KitTable};
41use serde_json::{Map, Value};
42
43#[derive(Debug, Clone)]
45pub struct PushdownPlan {
46 pub conditions: Vec<Condition>,
48 pub fully_translated: bool,
51}
52
53impl PushdownPlan {
54 pub fn can_push(&self) -> bool {
56 !self.conditions.is_empty()
57 }
58}
59
60pub fn translate_predicate(table: &KitTable, expr: &Expr) -> Option<PushdownPlan> {
64 let mut conditions = Vec::new();
65 let fully = collect_conditions(table, expr, &mut conditions);
66 if conditions.is_empty() {
67 return None;
68 }
69 Some(PushdownPlan {
70 conditions,
71 fully_translated: fully,
72 })
73}
74
75fn collect_conditions(table: &KitTable, expr: &Expr, out: &mut Vec<Condition>) -> bool {
78 match expr {
79 Expr::And(parts) => {
80 let mut all = true;
81 for part in parts {
82 if !collect_conditions(table, part, out) {
83 all = false;
84 }
85 }
86 all
87 }
88 Expr::Eq(a, b) => push_if_some(out, try_translate_eq(table, a, b)),
89 Expr::Lt(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Lt)),
90 Expr::Lte(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Lte)),
91 Expr::Gt(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Gt)),
92 Expr::Gte(a, b) => push_if_some(out, try_translate_cmp(table, a, b, CmpOp::Gte)),
93 Expr::In(a, list) => push_if_some(out, try_translate_in(table, a, list)),
94 Expr::Contains(a, needle) => {
95 if let Some(c) = try_translate_contains(table, a, needle) {
99 out.push(c);
100 }
101 false
102 }
103 Expr::Like(a, pattern) => {
104 if let Some(c) = try_translate_like(table, a, pattern) {
107 out.push(c);
108 }
109 false
110 }
111 Expr::IsNull(a) => {
112 if let Some(c) = try_translate_null(table, a, true) {
113 out.push(c);
114 }
115 false
116 }
117 Expr::IsNotNull(a) => {
118 if let Some(c) = try_translate_null(table, a, false) {
119 out.push(c);
120 }
121 false
122 }
123 _ => false,
126 }
127}
128
129fn try_translate_null(table: &KitTable, a: &Expr, is_null: bool) -> Option<Condition> {
132 let Expr::Column(col_name) = a else {
133 return None;
134 };
135 let column_id = table.column(col_name)?.id as u16;
136 Some(if is_null {
137 Condition::IsNull { column_id }
138 } else {
139 Condition::IsNotNull { column_id }
140 })
141}
142
143fn push_if_some(out: &mut Vec<Condition>, opt: Option<Condition>) -> bool {
145 match opt {
146 Some(c) => {
147 out.push(c);
148 true
149 }
150 None => false,
151 }
152}
153
154#[derive(Clone, Copy)]
155enum CmpOp {
156 Lt,
157 Lte,
158 Gt,
159 Gte,
160}
161
162fn extract_column_literal<'a>(a: &'a Expr, b: &'a Expr) -> Option<(&'a str, &'a Literal)> {
165 match (a, b) {
166 (Expr::Column(name), Expr::Literal(lit)) => Some((name.as_str(), lit)),
167 (Expr::Literal(lit), Expr::Column(name)) => Some((name.as_str(), lit)),
168 _ => None, }
170}
171
172fn try_translate_eq(table: &KitTable, a: &Expr, b: &Expr) -> Option<Condition> {
174 let (col_name, lit) = extract_column_literal(a, b)?;
175 let col = table.column(col_name)?;
176 let col_id = col.id as u16;
177 let ty = col.storage_type;
178
179 if table.primary_key.len() == 1 && table.primary_key[0] == col_name {
181 let key = literal_to_index_key(lit, ty)?;
182 return Some(Condition::Pk(key));
183 }
184
185 if has_bitmap_index(table, col_name) {
188 let value = literal_to_index_key(lit, ty)?;
189 return Some(Condition::BitmapEq {
190 column_id: col_id,
191 value,
192 });
193 }
194
195 None
196}
197
198fn try_translate_cmp(table: &KitTable, a: &Expr, b: &Expr, op: CmpOp) -> Option<Condition> {
200 let (col_name, lit) = extract_column_literal(a, b)?;
201 let col = table.column(col_name)?;
202 let col_id = col.id as u16;
203 let ty = col.storage_type;
204
205 if is_int_type(ty) {
206 let v = literal_to_i64(lit)?;
207 let (lo, hi) = match op {
208 CmpOp::Lt => (i64::MIN, v.saturating_sub(1)),
209 CmpOp::Lte => (i64::MIN, v),
210 CmpOp::Gt => (v.saturating_add(1), i64::MAX),
211 CmpOp::Gte => (v, i64::MAX),
212 };
213 Some(Condition::Range {
214 column_id: col_id,
215 lo,
216 hi,
217 })
218 } else if is_float_type(ty) {
219 let v = literal_to_f64(lit)?;
220 let (lo, lo_inc, hi, hi_inc) = match op {
221 CmpOp::Lt => (f64::NEG_INFINITY, false, v, false),
222 CmpOp::Lte => (f64::NEG_INFINITY, false, v, true),
223 CmpOp::Gt => (v, false, f64::INFINITY, false),
224 CmpOp::Gte => (v, true, f64::INFINITY, false),
225 };
226 Some(Condition::RangeF64 {
227 column_id: col_id,
228 lo,
229 lo_inclusive: lo_inc,
230 hi,
231 hi_inclusive: hi_inc,
232 })
233 } else {
234 None
235 }
236}
237
238fn has_fm_index(table: &KitTable, col_name: &str) -> bool {
240 table
241 .indexes
242 .iter()
243 .any(|idx| idx.kind == KitIndexKind::Fm && idx.columns.iter().any(|c| c == col_name))
244}
245
246fn try_translate_like(table: &KitTable, a: &Expr, pattern: &str) -> Option<Condition> {
252 let Expr::Column(col_name) = a else {
253 return None;
254 };
255 let col = table.column(col_name)?;
256 if !has_fm_index(table, col_name) || pattern.contains('\\') {
257 return None;
258 }
259 let patterns: Vec<Vec<u8>> = pattern
260 .split(['%', '_'])
261 .filter(|s| !s.is_empty())
262 .map(|s| s.as_bytes().to_vec())
263 .collect();
264 if patterns.is_empty() {
265 return None;
266 }
267 Some(Condition::FmContainsAll {
268 column_id: col.id as u16,
269 patterns,
270 })
271}
272
273fn try_translate_contains(table: &KitTable, a: &Expr, needle: &str) -> Option<Condition> {
277 let Expr::Column(col_name) = a else {
278 return None;
279 };
280 let col = table.column(col_name)?;
281 if !has_fm_index(table, col_name) {
282 return None;
283 }
284 Some(Condition::FmContains {
285 column_id: col.id as u16,
286 pattern: needle.as_bytes().to_vec(),
287 })
288}
289
290fn try_translate_in(table: &KitTable, a: &Expr, list: &[Literal]) -> Option<Condition> {
292 let Expr::Column(col_name) = a else {
293 return None;
294 };
295 let col = table.column(col_name)?;
296 let col_id = col.id as u16;
297 let ty = col.storage_type;
298 if !has_bitmap_index(table, col_name) {
299 return None;
300 }
301 let mut values = Vec::with_capacity(list.len());
302 for lit in list {
303 values.push(literal_to_index_key(lit, ty)?);
304 }
305 Some(Condition::BitmapIn {
306 column_id: col_id,
307 values,
308 })
309}
310
311pub fn pk_conditions(table: &KitTable, pk_map: &Map<String, Value>) -> Option<Vec<Condition>> {
319 if pk_map.is_empty() || table.primary_key.is_empty() {
320 return None;
321 }
322 let mut conditions = Vec::with_capacity(table.primary_key.len());
323 for pk_name in &table.primary_key {
324 let value = pk_map.get(pk_name)?;
325 let col = table.column(pk_name)?;
326 let lit = json_to_literal(value)?;
327 let key = literal_to_index_key(&lit, col.storage_type)?;
328 if table.primary_key.len() == 1 {
329 conditions.push(Condition::Pk(key));
331 } else {
332 conditions.push(Condition::BitmapEq {
334 column_id: col.id as u16,
335 value: key,
336 });
337 }
338 }
339 Some(conditions)
340}
341
342fn has_bitmap_index(table: &KitTable, col_name: &str) -> bool {
347 table
348 .indexes
349 .iter()
350 .any(|idx| idx.columns.iter().any(|c| c == col_name))
351 || table
352 .unique_constraints
353 .iter()
354 .any(|uq| uq.columns.iter().any(|c| c == col_name))
355 || table.primary_key.contains(&col_name.to_string())
356}
357
358pub(crate) fn has_declared_bitmap_index(table: &KitTable, col_name: &str) -> bool {
364 table
365 .indexes
366 .iter()
367 .any(|idx| idx.columns.iter().any(|c| c == col_name))
368 || table
369 .unique_constraints
370 .iter()
371 .any(|uq| uq.columns.iter().any(|c| c == col_name))
372}
373
374pub(crate) fn value_index_key(v: &Value, ty: ColumnType) -> Option<Vec<u8>> {
378 let lit = match v {
379 Value::Bool(b) => Literal::Bool(*b),
380 Value::Number(n) => n
381 .as_i64()
382 .map(Literal::Int)
383 .or_else(|| n.as_f64().map(Literal::Float))?,
384 Value::String(s) => Literal::Text(s.clone()),
385 _ => return None,
386 };
387 literal_to_index_key(&lit, ty)
388}
389
390fn is_int_type(ty: ColumnType) -> bool {
391 matches!(
392 ty,
393 ColumnType::Int8
394 | ColumnType::Int16
395 | ColumnType::Int32
396 | ColumnType::Int64
397 | ColumnType::Bool
398 | ColumnType::TimestampNanos
399 )
400}
401
402fn is_float_type(ty: ColumnType) -> bool {
403 matches!(ty, ColumnType::Float32 | ColumnType::Float64)
404}
405
406fn literal_to_index_key(lit: &Literal, ty: ColumnType) -> Option<Vec<u8>> {
409 match lit {
410 Literal::Null => None, Literal::Bool(b) => Some(vec![*b as u8]),
412 Literal::Int(n) => {
413 if is_float_type(ty) {
414 Some((*n as f64).to_bits().to_be_bytes().to_vec())
416 } else {
417 Some(n.to_be_bytes().to_vec())
418 }
419 }
420 Literal::Float(f) => Some(f.to_bits().to_be_bytes().to_vec()),
421 Literal::Text(s) => Some(s.as_bytes().to_vec()),
422 Literal::Json(v) => Some(serde_json::to_vec(v).ok()?),
423 }
424}
425
426fn literal_to_i64(lit: &Literal) -> Option<i64> {
427 match lit {
428 Literal::Int(n) => Some(*n),
429 Literal::Bool(b) => Some(*b as i64),
430 _ => None,
431 }
432}
433
434fn literal_to_f64(lit: &Literal) -> Option<f64> {
435 match lit {
436 Literal::Int(n) => Some(*n as f64),
437 Literal::Float(f) => Some(*f),
438 _ => None,
439 }
440}
441
442fn json_to_literal(value: &Value) -> Option<Literal> {
444 match value {
445 Value::Null => Some(Literal::Null),
446 Value::Bool(b) => Some(Literal::Bool(*b)),
447 Value::Number(n) => {
448 if let Some(i) = n.as_i64() {
449 Some(Literal::Int(i))
450 } else {
451 Some(Literal::Float(n.as_f64()?))
452 }
453 }
454 Value::String(s) => Some(Literal::Text(s.clone())),
455 Value::Array(_) | Value::Object(_) => Some(Literal::Json(value.clone())),
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use mongreldb_kit_core::query::Expr;
463 use mongreldb_kit_core::schema::{Column, Index, Table as KitTable};
464
465 fn sample_table() -> KitTable {
466 KitTable {
467 id: 1,
468 name: "users".into(),
469 columns: vec![
470 {
471 let mut c = Column::new(1, "id", ColumnType::Int64);
472 c.primary_key = true;
473 c
474 },
475 Column::new(2, "email", ColumnType::Text),
476 {
477 let mut c = Column::new(3, "age", ColumnType::Int64);
478 c.nullable = true;
479 c
480 },
481 {
482 let mut c = Column::new(4, "score", ColumnType::Float64);
483 c.nullable = true;
484 c
485 },
486 ],
487 primary_key: vec!["id".into()],
488 indexes: vec![Index {
489 name: "idx_email".into(),
490 columns: vec!["email".into()],
491 unique: false,
492 kind: Default::default(),
493 }],
494 foreign_keys: vec![],
495 unique_constraints: vec![],
496 check_constraints: vec![],
497 }
498 }
499
500 #[test]
501 fn translate_pk_eq() {
502 let t = sample_table();
503 let expr = Expr::Eq(
504 Box::new(Expr::Column("id".into())),
505 Box::new(Expr::Literal(Literal::Int(42))),
506 );
507 let plan = translate_predicate(&t, &expr).expect("should translate");
508 assert!(plan.fully_translated);
509 assert_eq!(plan.conditions.len(), 1);
510 assert!(matches!(plan.conditions[0], Condition::Pk(_)));
511 }
512
513 #[test]
514 fn translate_bitmap_eq() {
515 let t = sample_table();
516 let expr = Expr::Eq(
517 Box::new(Expr::Column("email".into())),
518 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
519 );
520 let plan = translate_predicate(&t, &expr).expect("should translate");
521 assert!(plan.fully_translated);
522 assert!(matches!(
523 plan.conditions[0],
524 Condition::BitmapEq { column_id: 2, .. }
525 ));
526 }
527
528 #[test]
529 fn translate_int_range() {
530 let t = sample_table();
531 let expr = Expr::Gte(
532 Box::new(Expr::Column("age".into())),
533 Box::new(Expr::Literal(Literal::Int(18))),
534 );
535 let plan = translate_predicate(&t, &expr).expect("should translate");
536 assert!(plan.fully_translated);
537 assert!(matches!(
538 plan.conditions[0],
539 Condition::Range {
540 column_id: 3,
541 lo: 18,
542 ..
543 }
544 ));
545 }
546
547 #[test]
548 fn translate_and_of_translatable() {
549 let t = sample_table();
550 let expr = Expr::And(vec![
551 Expr::Eq(
552 Box::new(Expr::Column("email".into())),
553 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
554 ),
555 Expr::Gt(
556 Box::new(Expr::Column("age".into())),
557 Box::new(Expr::Literal(Literal::Int(21))),
558 ),
559 ]);
560 let plan = translate_predicate(&t, &expr).expect("should translate");
561 assert!(plan.fully_translated);
562 assert_eq!(plan.conditions.len(), 2);
563 }
564
565 #[test]
566 fn translate_and_partial() {
567 let t = sample_table();
568 let expr = Expr::And(vec![
570 Expr::Eq(
571 Box::new(Expr::Column("email".into())),
572 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
573 ),
574 Expr::Or(vec![
575 Expr::Gt(
576 Box::new(Expr::Column("age".into())),
577 Box::new(Expr::Literal(Literal::Int(21))),
578 ),
579 Expr::Lt(
580 Box::new(Expr::Column("score".into())),
581 Box::new(Expr::Literal(Literal::Float(5.0))),
582 ),
583 ]),
584 ]);
585 let plan = translate_predicate(&t, &expr).expect("should partially translate");
586 assert!(!plan.fully_translated); assert_eq!(plan.conditions.len(), 1); }
589
590 #[test]
591 fn translate_unsupported_returns_none() {
592 let t = sample_table();
593 let expr = Expr::Or(vec![
594 Expr::Eq(
595 Box::new(Expr::Column("email".into())),
596 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
597 ),
598 Expr::Eq(
599 Box::new(Expr::Column("email".into())),
600 Box::new(Expr::Literal(Literal::Text("c@d.com".into()))),
601 ),
602 ]);
603 assert!(translate_predicate(&t, &expr).is_none());
604 }
605
606 #[test]
607 fn translate_in_to_bitmap_in() {
608 let t = sample_table();
609 let expr = Expr::In(
610 Box::new(Expr::Column("email".into())),
611 vec![
612 Literal::Text("a@b.com".into()),
613 Literal::Text("c@d.com".into()),
614 ],
615 );
616 let plan = translate_predicate(&t, &expr).expect("should translate");
617 assert!(plan.fully_translated);
618 assert!(matches!(
619 plan.conditions[0],
620 Condition::BitmapIn { column_id: 2, .. }
621 ));
622 }
623
624 #[test]
625 fn pk_conditions_single_col() {
626 let t = sample_table();
627 let mut pk_map = Map::new();
628 pk_map.insert("id".into(), Value::Number(42.into()));
629 let conds = pk_conditions(&t, &pk_map).expect("should build");
630 assert_eq!(conds.len(), 1);
631 assert!(matches!(conds[0], Condition::Pk(_)));
632 }
633}