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::BytesPrefix(a, prefix) => {
98 push_if_some(out, try_translate_bytes_prefix(table, a, prefix))
99 }
100 Expr::Contains(a, needle) => {
101 if let Some(c) = try_translate_contains(table, a, needle) {
105 out.push(c);
106 }
107 false
108 }
109 Expr::Like(a, pattern) => {
110 if let Some(c) = try_translate_like(table, a, pattern) {
113 out.push(c);
114 }
115 false
116 }
117 Expr::IsNull(a) => {
118 if let Some(c) = try_translate_null(table, a, true) {
119 out.push(c);
120 }
121 false
122 }
123 Expr::IsNotNull(a) => {
124 if let Some(c) = try_translate_null(table, a, false) {
125 out.push(c);
126 }
127 false
128 }
129 _ => false,
132 }
133}
134
135fn try_translate_null(table: &KitTable, a: &Expr, is_null: bool) -> Option<Condition> {
138 let Expr::Column(col_name) = a else {
139 return None;
140 };
141 let column_id = table.column(col_name)?.id as u16;
142 Some(if is_null {
143 Condition::IsNull { column_id }
144 } else {
145 Condition::IsNotNull { column_id }
146 })
147}
148
149fn push_if_some(out: &mut Vec<Condition>, opt: Option<Condition>) -> bool {
151 match opt {
152 Some(c) => {
153 out.push(c);
154 true
155 }
156 None => false,
157 }
158}
159
160#[derive(Clone, Copy)]
161enum CmpOp {
162 Lt,
163 Lte,
164 Gt,
165 Gte,
166}
167
168fn extract_column_literal<'a>(a: &'a Expr, b: &'a Expr) -> Option<(&'a str, &'a Literal)> {
171 match (a, b) {
172 (Expr::Column(name), Expr::Literal(lit)) => Some((name.as_str(), lit)),
173 (Expr::Literal(lit), Expr::Column(name)) => Some((name.as_str(), lit)),
174 _ => None, }
176}
177
178fn try_translate_eq(table: &KitTable, a: &Expr, b: &Expr) -> Option<Condition> {
180 let (col_name, lit) = extract_column_literal(a, b)?;
181 let col = table.column(col_name)?;
182 let col_id = col.id as u16;
183 let ty = col.storage_type;
184
185 if table.primary_key.len() == 1 && table.primary_key[0] == col_name {
187 let key = literal_to_index_key(lit, ty)?;
188 return Some(Condition::Pk(key));
189 }
190
191 if has_bitmap_index(table, col_name) {
194 let value = literal_to_index_key(lit, ty)?;
195 return Some(Condition::BitmapEq {
196 column_id: col_id,
197 value,
198 });
199 }
200
201 None
202}
203
204fn try_translate_cmp(table: &KitTable, a: &Expr, b: &Expr, op: CmpOp) -> Option<Condition> {
206 let (col_name, lit) = extract_column_literal(a, b)?;
207 let col = table.column(col_name)?;
208 let col_id = col.id as u16;
209 let ty = col.storage_type;
210
211 if is_int_type(ty) {
212 let v = literal_to_i64(lit)?;
213 let (lo, hi) = match op {
214 CmpOp::Lt => (i64::MIN, v.saturating_sub(1)),
215 CmpOp::Lte => (i64::MIN, v),
216 CmpOp::Gt => (v.saturating_add(1), i64::MAX),
217 CmpOp::Gte => (v, i64::MAX),
218 };
219 Some(Condition::Range {
220 column_id: col_id,
221 lo,
222 hi,
223 })
224 } else if is_float_type(ty) {
225 let v = literal_to_f64(lit)?;
226 let (lo, lo_inc, hi, hi_inc) = match op {
227 CmpOp::Lt => (f64::NEG_INFINITY, false, v, false),
228 CmpOp::Lte => (f64::NEG_INFINITY, false, v, true),
229 CmpOp::Gt => (v, false, f64::INFINITY, false),
230 CmpOp::Gte => (v, true, f64::INFINITY, false),
231 };
232 Some(Condition::RangeF64 {
233 column_id: col_id,
234 lo,
235 lo_inclusive: lo_inc,
236 hi,
237 hi_inclusive: hi_inc,
238 })
239 } else {
240 None
241 }
242}
243
244fn has_fm_index(table: &KitTable, col_name: &str) -> bool {
246 table
247 .indexes
248 .iter()
249 .any(|idx| idx.kind == KitIndexKind::Fm && idx.columns.iter().any(|c| c == col_name))
250}
251
252fn try_translate_like(table: &KitTable, a: &Expr, pattern: &str) -> Option<Condition> {
258 let Expr::Column(col_name) = a else {
259 return None;
260 };
261 let col = table.column(col_name)?;
262 if !has_fm_index(table, col_name) || pattern.contains('\\') {
263 return None;
264 }
265 let patterns: Vec<Vec<u8>> = pattern
266 .split(['%', '_'])
267 .filter(|s| !s.is_empty())
268 .map(|s| s.as_bytes().to_vec())
269 .collect();
270 if patterns.is_empty() {
271 return None;
272 }
273 Some(Condition::FmContainsAll {
274 column_id: col.id as u16,
275 patterns,
276 })
277}
278
279fn try_translate_contains(table: &KitTable, a: &Expr, needle: &str) -> Option<Condition> {
283 let Expr::Column(col_name) = a else {
284 return None;
285 };
286 let col = table.column(col_name)?;
287 if !has_fm_index(table, col_name) {
288 return None;
289 }
290 Some(Condition::FmContains {
291 column_id: col.id as u16,
292 pattern: needle.as_bytes().to_vec(),
293 })
294}
295
296fn try_translate_bytes_prefix(table: &KitTable, a: &Expr, prefix: &str) -> Option<Condition> {
302 let Expr::Column(col_name) = a else {
303 return None;
304 };
305 let col = table.column(col_name)?;
306 if !has_bitmap_index(table, col_name) {
307 return None;
308 }
309 Some(Condition::BytesPrefix {
310 column_id: col.id as u16,
311 prefix: prefix.as_bytes().to_vec(),
312 })
313}
314
315fn try_translate_in(table: &KitTable, a: &Expr, list: &[Literal]) -> Option<Condition> {
317 let Expr::Column(col_name) = a else {
318 return None;
319 };
320 let col = table.column(col_name)?;
321 let col_id = col.id as u16;
322 let ty = col.storage_type;
323 if !has_bitmap_index(table, col_name) {
324 return None;
325 }
326 let mut values = Vec::with_capacity(list.len());
327 for lit in list {
328 values.push(literal_to_index_key(lit, ty)?);
329 }
330 Some(Condition::BitmapIn {
331 column_id: col_id,
332 values,
333 })
334}
335
336pub fn pk_conditions(table: &KitTable, pk_map: &Map<String, Value>) -> Option<Vec<Condition>> {
344 if pk_map.is_empty() || table.primary_key.is_empty() {
345 return None;
346 }
347 let mut conditions = Vec::with_capacity(table.primary_key.len());
348 for pk_name in &table.primary_key {
349 let value = pk_map.get(pk_name)?;
350 let col = table.column(pk_name)?;
351 let lit = json_to_literal(value)?;
352 let key = literal_to_index_key(&lit, col.storage_type)?;
353 if table.primary_key.len() == 1 {
354 conditions.push(Condition::Pk(key));
356 } else {
357 conditions.push(Condition::BitmapEq {
359 column_id: col.id as u16,
360 value: key,
361 });
362 }
363 }
364 Some(conditions)
365}
366
367fn has_bitmap_index(table: &KitTable, col_name: &str) -> bool {
372 table
373 .indexes
374 .iter()
375 .any(|idx| idx.columns.iter().any(|c| c == col_name))
376 || table
377 .unique_constraints
378 .iter()
379 .any(|uq| uq.columns.iter().any(|c| c == col_name))
380 || table.primary_key.contains(&col_name.to_string())
381}
382
383pub(crate) fn has_declared_bitmap_index(table: &KitTable, col_name: &str) -> bool {
389 table
390 .indexes
391 .iter()
392 .any(|idx| idx.columns.iter().any(|c| c == col_name))
393 || table
394 .unique_constraints
395 .iter()
396 .any(|uq| uq.columns.iter().any(|c| c == col_name))
397}
398
399pub(crate) fn value_index_key(v: &Value, ty: ColumnType) -> Option<Vec<u8>> {
403 let lit = match v {
404 Value::Bool(b) => Literal::Bool(*b),
405 Value::Number(n) => n
406 .as_i64()
407 .map(Literal::Int)
408 .or_else(|| n.as_f64().map(Literal::Float))?,
409 Value::String(s) => Literal::Text(s.clone()),
410 _ => return None,
411 };
412 literal_to_index_key(&lit, ty)
413}
414
415fn is_int_type(ty: ColumnType) -> bool {
416 matches!(
417 ty,
418 ColumnType::Int8
419 | ColumnType::Int16
420 | ColumnType::Int32
421 | ColumnType::Int64
422 | ColumnType::Bool
423 | ColumnType::TimestampNanos
424 )
425}
426
427fn is_float_type(ty: ColumnType) -> bool {
428 matches!(ty, ColumnType::Float32 | ColumnType::Float64)
429}
430
431fn literal_to_index_key(lit: &Literal, ty: ColumnType) -> Option<Vec<u8>> {
434 match lit {
435 Literal::Null => None, Literal::Bool(b) => Some(vec![*b as u8]),
437 Literal::Int(n) => {
438 if is_float_type(ty) {
439 Some((*n as f64).to_bits().to_be_bytes().to_vec())
441 } else {
442 Some(n.to_be_bytes().to_vec())
443 }
444 }
445 Literal::Float(f) => Some(f.to_bits().to_be_bytes().to_vec()),
446 Literal::Text(s) => Some(s.as_bytes().to_vec()),
447 Literal::Json(v) => Some(serde_json::to_vec(v).ok()?),
448 }
449}
450
451fn literal_to_i64(lit: &Literal) -> Option<i64> {
452 match lit {
453 Literal::Int(n) => Some(*n),
454 Literal::Bool(b) => Some(*b as i64),
455 _ => None,
456 }
457}
458
459fn literal_to_f64(lit: &Literal) -> Option<f64> {
460 match lit {
461 Literal::Int(n) => Some(*n as f64),
462 Literal::Float(f) => Some(*f),
463 _ => None,
464 }
465}
466
467fn json_to_literal(value: &Value) -> Option<Literal> {
469 match value {
470 Value::Null => Some(Literal::Null),
471 Value::Bool(b) => Some(Literal::Bool(*b)),
472 Value::Number(n) => {
473 if let Some(i) = n.as_i64() {
474 Some(Literal::Int(i))
475 } else {
476 Some(Literal::Float(n.as_f64()?))
477 }
478 }
479 Value::String(s) => Some(Literal::Text(s.clone())),
480 Value::Array(_) | Value::Object(_) => Some(Literal::Json(value.clone())),
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use mongreldb_kit_core::query::Expr;
488 use mongreldb_kit_core::schema::{Column, Index, Table as KitTable};
489
490 fn sample_table() -> KitTable {
491 KitTable {
492 id: 1,
493 name: "users".into(),
494 columns: vec![
495 {
496 let mut c = Column::new(1, "id", ColumnType::Int64);
497 c.primary_key = true;
498 c
499 },
500 Column::new(2, "email", ColumnType::Text),
501 {
502 let mut c = Column::new(3, "age", ColumnType::Int64);
503 c.nullable = true;
504 c
505 },
506 {
507 let mut c = Column::new(4, "score", ColumnType::Float64);
508 c.nullable = true;
509 c
510 },
511 ],
512 primary_key: vec!["id".into()],
513 indexes: vec![Index {
514 name: "idx_email".into(),
515 columns: vec!["email".into()],
516 unique: false,
517 kind: Default::default(),
518 }],
519 foreign_keys: vec![],
520 unique_constraints: vec![],
521 check_constraints: vec![],
522 }
523 }
524
525 #[test]
526 fn translate_pk_eq() {
527 let t = sample_table();
528 let expr = Expr::Eq(
529 Box::new(Expr::Column("id".into())),
530 Box::new(Expr::Literal(Literal::Int(42))),
531 );
532 let plan = translate_predicate(&t, &expr).expect("should translate");
533 assert!(plan.fully_translated);
534 assert_eq!(plan.conditions.len(), 1);
535 assert!(matches!(plan.conditions[0], Condition::Pk(_)));
536 }
537
538 #[test]
539 fn translate_bitmap_eq() {
540 let t = sample_table();
541 let expr = Expr::Eq(
542 Box::new(Expr::Column("email".into())),
543 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
544 );
545 let plan = translate_predicate(&t, &expr).expect("should translate");
546 assert!(plan.fully_translated);
547 assert!(matches!(
548 plan.conditions[0],
549 Condition::BitmapEq { column_id: 2, .. }
550 ));
551 }
552
553 #[test]
554 fn translate_int_range() {
555 let t = sample_table();
556 let expr = Expr::Gte(
557 Box::new(Expr::Column("age".into())),
558 Box::new(Expr::Literal(Literal::Int(18))),
559 );
560 let plan = translate_predicate(&t, &expr).expect("should translate");
561 assert!(plan.fully_translated);
562 assert!(matches!(
563 plan.conditions[0],
564 Condition::Range {
565 column_id: 3,
566 lo: 18,
567 ..
568 }
569 ));
570 }
571
572 #[test]
573 fn translate_and_of_translatable() {
574 let t = sample_table();
575 let expr = Expr::And(vec![
576 Expr::Eq(
577 Box::new(Expr::Column("email".into())),
578 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
579 ),
580 Expr::Gt(
581 Box::new(Expr::Column("age".into())),
582 Box::new(Expr::Literal(Literal::Int(21))),
583 ),
584 ]);
585 let plan = translate_predicate(&t, &expr).expect("should translate");
586 assert!(plan.fully_translated);
587 assert_eq!(plan.conditions.len(), 2);
588 }
589
590 #[test]
591 fn translate_and_partial() {
592 let t = sample_table();
593 let expr = Expr::And(vec![
595 Expr::Eq(
596 Box::new(Expr::Column("email".into())),
597 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
598 ),
599 Expr::Or(vec![
600 Expr::Gt(
601 Box::new(Expr::Column("age".into())),
602 Box::new(Expr::Literal(Literal::Int(21))),
603 ),
604 Expr::Lt(
605 Box::new(Expr::Column("score".into())),
606 Box::new(Expr::Literal(Literal::Float(5.0))),
607 ),
608 ]),
609 ]);
610 let plan = translate_predicate(&t, &expr).expect("should partially translate");
611 assert!(!plan.fully_translated); assert_eq!(plan.conditions.len(), 1); }
614
615 #[test]
616 fn translate_unsupported_returns_none() {
617 let t = sample_table();
618 let expr = Expr::Or(vec![
619 Expr::Eq(
620 Box::new(Expr::Column("email".into())),
621 Box::new(Expr::Literal(Literal::Text("a@b.com".into()))),
622 ),
623 Expr::Eq(
624 Box::new(Expr::Column("email".into())),
625 Box::new(Expr::Literal(Literal::Text("c@d.com".into()))),
626 ),
627 ]);
628 assert!(translate_predicate(&t, &expr).is_none());
629 }
630
631 #[test]
632 fn translate_in_to_bitmap_in() {
633 let t = sample_table();
634 let expr = Expr::In(
635 Box::new(Expr::Column("email".into())),
636 vec![
637 Literal::Text("a@b.com".into()),
638 Literal::Text("c@d.com".into()),
639 ],
640 );
641 let plan = translate_predicate(&t, &expr).expect("should translate");
642 assert!(plan.fully_translated);
643 assert!(matches!(
644 plan.conditions[0],
645 Condition::BitmapIn { column_id: 2, .. }
646 ));
647 }
648
649 #[test]
650 fn pk_conditions_single_col() {
651 let t = sample_table();
652 let mut pk_map = Map::new();
653 pk_map.insert("id".into(), Value::Number(42.into()));
654 let conds = pk_conditions(&t, &pk_map).expect("should build");
655 assert_eq!(conds.len(), 1);
656 assert!(matches!(conds[0], Condition::Pk(_)));
657 }
658}