1use crate::{
7 db::{
8 QueryError,
9 predicate::coercion::{CoercionId, CoercionSpec},
10 sql::lowering::{
11 PreparedSqlPredicateTemplateShape, sql_expr_prepared_predicate_template_shape,
12 },
13 },
14 value::Value,
15};
16use std::ops::{BitAnd, BitOr};
17use thiserror::Error as ThisError;
18
19#[cfg_attr(doc, doc = "Predicate")]
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum Predicate {
22 True,
23 False,
24 And(Vec<Self>),
25 Or(Vec<Self>),
26 Not(Box<Self>),
27 Compare(ComparePredicate),
28 CompareFields(CompareFieldsPredicate),
29 IsNull { field: String },
30 IsNotNull { field: String },
31 IsMissing { field: String },
32 IsEmpty { field: String },
33 IsNotEmpty { field: String },
34 TextContains { field: String, value: Value },
35 TextContainsCi { field: String, value: Value },
36}
37
38#[derive(Clone, Debug)]
47pub(in crate::db) struct PreparedSqlScalarCompareSlotTemplate {
48 field: String,
49 op: CompareOp,
50 coercion: CoercionId,
51 slot_index: usize,
52}
53
54#[derive(Clone, Debug)]
63pub(in crate::db) enum PreparedSqlScalarPredicateTemplate {
64 Compare(PreparedSqlScalarCompareSlotTemplate),
65 And(Vec<Self>),
66 Or(Vec<Self>),
67 Not(Box<Self>),
68}
69
70impl Predicate {
71 #[must_use]
73 pub const fn and(preds: Vec<Self>) -> Self {
74 Self::And(preds)
75 }
76
77 #[must_use]
79 pub const fn or(preds: Vec<Self>) -> Self {
80 Self::Or(preds)
81 }
82
83 #[must_use]
85 #[expect(clippy::should_implement_trait)]
86 pub fn not(pred: Self) -> Self {
87 Self::Not(Box::new(pred))
88 }
89
90 #[must_use]
92 pub fn eq(field: String, value: Value) -> Self {
93 Self::Compare(ComparePredicate::eq(field, value))
94 }
95
96 #[must_use]
98 pub fn ne(field: String, value: Value) -> Self {
99 Self::Compare(ComparePredicate::ne(field, value))
100 }
101
102 #[must_use]
104 pub fn lt(field: String, value: Value) -> Self {
105 Self::Compare(ComparePredicate::lt(field, value))
106 }
107
108 #[must_use]
110 pub fn lte(field: String, value: Value) -> Self {
111 Self::Compare(ComparePredicate::lte(field, value))
112 }
113
114 #[must_use]
116 pub fn gt(field: String, value: Value) -> Self {
117 Self::Compare(ComparePredicate::gt(field, value))
118 }
119
120 #[must_use]
122 pub fn gte(field: String, value: Value) -> Self {
123 Self::Compare(ComparePredicate::gte(field, value))
124 }
125
126 #[must_use]
128 pub fn eq_fields(left_field: String, right_field: String) -> Self {
129 Self::CompareFields(CompareFieldsPredicate::eq(left_field, right_field))
130 }
131
132 #[must_use]
134 pub fn ne_fields(left_field: String, right_field: String) -> Self {
135 Self::CompareFields(CompareFieldsPredicate::ne(left_field, right_field))
136 }
137
138 #[must_use]
140 pub fn lt_fields(left_field: String, right_field: String) -> Self {
141 Self::CompareFields(CompareFieldsPredicate::with_coercion(
142 left_field,
143 CompareOp::Lt,
144 right_field,
145 CoercionId::NumericWiden,
146 ))
147 }
148
149 #[must_use]
151 pub fn lte_fields(left_field: String, right_field: String) -> Self {
152 Self::CompareFields(CompareFieldsPredicate::with_coercion(
153 left_field,
154 CompareOp::Lte,
155 right_field,
156 CoercionId::NumericWiden,
157 ))
158 }
159
160 #[must_use]
162 pub fn gt_fields(left_field: String, right_field: String) -> Self {
163 Self::CompareFields(CompareFieldsPredicate::with_coercion(
164 left_field,
165 CompareOp::Gt,
166 right_field,
167 CoercionId::NumericWiden,
168 ))
169 }
170
171 #[must_use]
173 pub fn gte_fields(left_field: String, right_field: String) -> Self {
174 Self::CompareFields(CompareFieldsPredicate::with_coercion(
175 left_field,
176 CompareOp::Gte,
177 right_field,
178 CoercionId::NumericWiden,
179 ))
180 }
181
182 #[must_use]
184 pub fn in_(field: String, values: Vec<Value>) -> Self {
185 Self::Compare(ComparePredicate::in_(field, values))
186 }
187
188 #[must_use]
190 pub fn not_in(field: String, values: Vec<Value>) -> Self {
191 Self::Compare(ComparePredicate::not_in(field, values))
192 }
193
194 #[must_use]
196 pub const fn is_not_null(field: String) -> Self {
197 Self::IsNotNull { field }
198 }
199
200 #[must_use]
202 pub fn between(field: String, lower: Value, upper: Value) -> Self {
203 Self::And(vec![
204 Self::gte(field.clone(), lower),
205 Self::lte(field, upper),
206 ])
207 }
208
209 #[must_use]
211 pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
212 Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
213 }
214
215 #[must_use]
218 pub(in crate::db) fn contains_any_runtime_values(&self, candidates: &[Value]) -> bool {
219 match self {
220 Self::True
221 | Self::False
222 | Self::CompareFields(_)
223 | Self::IsNull { .. }
224 | Self::IsNotNull { .. }
225 | Self::IsMissing { .. }
226 | Self::IsEmpty { .. }
227 | Self::IsNotEmpty { .. } => false,
228 Self::Compare(compare) => candidates.contains(compare.value()),
229 Self::And(children) | Self::Or(children) => children
230 .iter()
231 .any(|child| child.contains_any_runtime_values(candidates)),
232 Self::Not(child) => child.contains_any_runtime_values(candidates),
233 Self::TextContains { value, .. } | Self::TextContainsCi { value, .. } => {
234 candidates.contains(value)
235 }
236 }
237 }
238
239 #[must_use]
242 pub(in crate::db) fn bind_template_values(self, replacements: &[(Value, Value)]) -> Self {
243 match self {
244 Self::True => Self::True,
245 Self::False => Self::False,
246 Self::And(children) => Self::And(
247 children
248 .into_iter()
249 .map(|child| child.bind_template_values(replacements))
250 .collect(),
251 ),
252 Self::Or(children) => Self::Or(
253 children
254 .into_iter()
255 .map(|child| child.bind_template_values(replacements))
256 .collect(),
257 ),
258 Self::Not(child) => Self::Not(Box::new(child.bind_template_values(replacements))),
259 Self::Compare(compare) => Self::Compare(ComparePredicate::with_coercion(
260 compare.field,
261 compare.op,
262 bind_template_value(compare.value, replacements),
263 compare.coercion.id,
264 )),
265 Self::CompareFields(compare) => Self::CompareFields(compare),
266 Self::IsNull { field } => Self::IsNull { field },
267 Self::IsNotNull { field } => Self::IsNotNull { field },
268 Self::IsMissing { field } => Self::IsMissing { field },
269 Self::IsEmpty { field } => Self::IsEmpty { field },
270 Self::IsNotEmpty { field } => Self::IsNotEmpty { field },
271 Self::TextContains { field, value } => Self::TextContains {
272 field,
273 value: bind_template_value(value, replacements),
274 },
275 Self::TextContainsCi { field, value } => Self::TextContainsCi {
276 field,
277 value: bind_template_value(value, replacements),
278 },
279 }
280 }
281
282 #[must_use]
285 pub(in crate::db) fn build_prepared_template(
286 &self,
287 shape: PreparedSqlPredicateTemplateShape<'_>,
288 ) -> Option<PreparedSqlScalarPredicateTemplate> {
289 match (shape, self) {
290 (PreparedSqlPredicateTemplateShape::And { left, right }, Self::And(children))
291 if children.len() == 2 =>
292 {
293 Self::build_prepared_binary_children_template(
294 left,
295 right,
296 &children[0],
297 &children[1],
298 PreparedSqlScalarPredicateTemplate::And,
299 )
300 }
301 (PreparedSqlPredicateTemplateShape::Or { left, right }, Self::Or(children))
302 if children.len() == 2 =>
303 {
304 Self::build_prepared_binary_children_template(
305 left,
306 right,
307 &children[0],
308 &children[1],
309 PreparedSqlScalarPredicateTemplate::Or,
310 )
311 }
312 (PreparedSqlPredicateTemplateShape::Not { expr }, Self::Not(child)) => {
313 Some(PreparedSqlScalarPredicateTemplate::Not(Box::new(
314 child.build_prepared_template(sql_expr_prepared_predicate_template_shape(
315 expr,
316 )?)?,
317 )))
318 }
319 (
320 PreparedSqlPredicateTemplateShape::CompareWithParamRhs { slot_index },
321 Self::Compare(compare),
322 ) => Some(PreparedSqlScalarPredicateTemplate::Compare(
323 PreparedSqlScalarCompareSlotTemplate {
324 field: compare.field.clone(),
325 op: compare.op,
326 coercion: compare.coercion.id,
327 slot_index,
328 },
329 )),
330 _ => None,
331 }
332 }
333
334 fn build_prepared_binary_children_template(
335 left_sql: &crate::db::sql::parser::SqlExpr,
336 right_sql: &crate::db::sql::parser::SqlExpr,
337 first_child: &Self,
338 second_child: &Self,
339 ctor: fn(Vec<PreparedSqlScalarPredicateTemplate>) -> PreparedSqlScalarPredicateTemplate,
340 ) -> Option<PreparedSqlScalarPredicateTemplate> {
341 if let (Some(left), Some(right)) = (
342 first_child
343 .build_prepared_template(sql_expr_prepared_predicate_template_shape(left_sql)?),
344 second_child
345 .build_prepared_template(sql_expr_prepared_predicate_template_shape(right_sql)?),
346 ) {
347 return Some(ctor(vec![left, right]));
348 }
349
350 let (Some(left), Some(right)) = (
351 second_child
352 .build_prepared_template(sql_expr_prepared_predicate_template_shape(left_sql)?),
353 first_child
354 .build_prepared_template(sql_expr_prepared_predicate_template_shape(right_sql)?),
355 ) else {
356 return None;
357 };
358
359 Some(ctor(vec![left, right]))
360 }
361}
362
363impl PreparedSqlScalarPredicateTemplate {
364 pub(in crate::db) fn instantiate(&self, bindings: &[Value]) -> Result<Predicate, QueryError> {
367 match self {
368 Self::Compare(compare) => {
369 let binding = bindings
370 .get(compare.slot_index)
371 .ok_or_else(|| {
372 QueryError::unsupported_query(format!(
373 "missing prepared SQL binding at index={}",
374 compare.slot_index,
375 ))
376 })?
377 .clone();
378
379 Ok(Predicate::Compare(ComparePredicate::with_coercion(
380 compare.field.clone(),
381 compare.op,
382 binding,
383 compare.coercion,
384 )))
385 }
386 Self::And(children) => Ok(Predicate::And(
387 children
388 .iter()
389 .map(|child| child.instantiate(bindings))
390 .collect::<Result<Vec<_>, _>>()?,
391 )),
392 Self::Or(children) => Ok(Predicate::Or(
393 children
394 .iter()
395 .map(|child| child.instantiate(bindings))
396 .collect::<Result<Vec<_>, _>>()?,
397 )),
398 Self::Not(child) => Ok(Predicate::Not(Box::new(child.instantiate(bindings)?))),
399 }
400 }
401}
402
403fn bind_template_value(value: Value, replacements: &[(Value, Value)]) -> Value {
404 replacements
405 .iter()
406 .find(|(template, _)| *template == value)
407 .map_or(value, |(_, bound)| bound.clone())
408}
409
410impl BitAnd for Predicate {
411 type Output = Self;
412
413 fn bitand(self, rhs: Self) -> Self::Output {
414 Self::And(vec![self, rhs])
415 }
416}
417
418impl BitAnd for &Predicate {
419 type Output = Predicate;
420
421 fn bitand(self, rhs: Self) -> Self::Output {
422 Predicate::And(vec![self.clone(), rhs.clone()])
423 }
424}
425
426impl BitOr for Predicate {
427 type Output = Self;
428
429 fn bitor(self, rhs: Self) -> Self::Output {
430 Self::Or(vec![self, rhs])
431 }
432}
433
434impl BitOr for &Predicate {
435 type Output = Predicate;
436
437 fn bitor(self, rhs: Self) -> Self::Output {
438 Predicate::Or(vec![self.clone(), rhs.clone()])
439 }
440}
441
442#[cfg_attr(doc, doc = "CompareOp")]
443#[derive(Clone, Copy, Debug, Eq, PartialEq)]
444#[repr(u8)]
445pub enum CompareOp {
446 Eq = 0x01,
447 Ne = 0x02,
448 Lt = 0x03,
449 Lte = 0x04,
450 Gt = 0x05,
451 Gte = 0x06,
452 In = 0x07,
453 NotIn = 0x08,
454 Contains = 0x09,
455 StartsWith = 0x0a,
456 EndsWith = 0x0b,
457}
458
459impl CompareOp {
460 #[must_use]
462 pub const fn tag(self) -> u8 {
463 self as u8
464 }
465
466 #[must_use]
468 pub const fn flipped(self) -> Self {
469 match self {
470 Self::Eq => Self::Eq,
471 Self::Ne => Self::Ne,
472 Self::Lt => Self::Gt,
473 Self::Lte => Self::Gte,
474 Self::Gt => Self::Lt,
475 Self::Gte => Self::Lte,
476 Self::In => Self::In,
477 Self::NotIn => Self::NotIn,
478 Self::Contains => Self::Contains,
479 Self::StartsWith => Self::StartsWith,
480 Self::EndsWith => Self::EndsWith,
481 }
482 }
483}
484
485#[cfg_attr(doc, doc = "ComparePredicate")]
486#[derive(Clone, Debug, Eq, PartialEq)]
487pub struct ComparePredicate {
488 pub(crate) field: String,
489 pub(crate) op: CompareOp,
490 pub(crate) value: Value,
491 pub(crate) coercion: CoercionSpec,
492}
493
494impl ComparePredicate {
495 fn new(field: String, op: CompareOp, value: Value) -> Self {
496 Self {
497 field,
498 op,
499 value,
500 coercion: CoercionSpec::default(),
501 }
502 }
503
504 #[must_use]
506 pub fn with_coercion(
507 field: impl Into<String>,
508 op: CompareOp,
509 value: Value,
510 coercion: CoercionId,
511 ) -> Self {
512 Self {
513 field: field.into(),
514 op,
515 value,
516 coercion: CoercionSpec::new(coercion),
517 }
518 }
519
520 #[must_use]
522 pub fn eq(field: String, value: Value) -> Self {
523 Self::new(field, CompareOp::Eq, value)
524 }
525
526 #[must_use]
528 pub fn ne(field: String, value: Value) -> Self {
529 Self::new(field, CompareOp::Ne, value)
530 }
531
532 #[must_use]
534 pub fn lt(field: String, value: Value) -> Self {
535 Self::new(field, CompareOp::Lt, value)
536 }
537
538 #[must_use]
540 pub fn lte(field: String, value: Value) -> Self {
541 Self::new(field, CompareOp::Lte, value)
542 }
543
544 #[must_use]
546 pub fn gt(field: String, value: Value) -> Self {
547 Self::new(field, CompareOp::Gt, value)
548 }
549
550 #[must_use]
552 pub fn gte(field: String, value: Value) -> Self {
553 Self::new(field, CompareOp::Gte, value)
554 }
555
556 #[must_use]
558 pub fn in_(field: String, values: Vec<Value>) -> Self {
559 Self::new(field, CompareOp::In, Value::List(values))
560 }
561
562 #[must_use]
564 pub fn not_in(field: String, values: Vec<Value>) -> Self {
565 Self::new(field, CompareOp::NotIn, Value::List(values))
566 }
567
568 #[must_use]
570 pub fn field(&self) -> &str {
571 &self.field
572 }
573
574 #[must_use]
576 pub const fn op(&self) -> CompareOp {
577 self.op
578 }
579
580 #[must_use]
582 pub const fn value(&self) -> &Value {
583 &self.value
584 }
585
586 #[must_use]
588 pub const fn coercion(&self) -> &CoercionSpec {
589 &self.coercion
590 }
591}
592
593#[derive(Clone, Debug, Eq, PartialEq)]
602pub struct CompareFieldsPredicate {
603 pub(crate) left_field: String,
604 pub(crate) op: CompareOp,
605 pub(crate) right_field: String,
606 pub(crate) coercion: CoercionSpec,
607}
608
609impl CompareFieldsPredicate {
610 fn canonicalize_symmetric_fields(
611 op: CompareOp,
612 left_field: String,
613 right_field: String,
614 ) -> (String, String) {
615 if matches!(op, CompareOp::Eq | CompareOp::Ne) && left_field < right_field {
616 (right_field, left_field)
617 } else {
618 (left_field, right_field)
619 }
620 }
621
622 fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
623 let (left_field, right_field) =
624 Self::canonicalize_symmetric_fields(op, left_field, right_field);
625
626 Self {
627 left_field,
628 op,
629 right_field,
630 coercion: CoercionSpec::default(),
631 }
632 }
633
634 #[must_use]
637 pub fn with_coercion(
638 left_field: impl Into<String>,
639 op: CompareOp,
640 right_field: impl Into<String>,
641 coercion: CoercionId,
642 ) -> Self {
643 let (left_field, right_field) =
644 Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
645
646 Self {
647 left_field,
648 op,
649 right_field,
650 coercion: CoercionSpec::new(coercion),
651 }
652 }
653
654 #[must_use]
656 pub fn eq(left_field: String, right_field: String) -> Self {
657 Self::new(left_field, CompareOp::Eq, right_field)
658 }
659
660 #[must_use]
662 pub fn ne(left_field: String, right_field: String) -> Self {
663 Self::new(left_field, CompareOp::Ne, right_field)
664 }
665
666 #[must_use]
668 pub fn lt(left_field: String, right_field: String) -> Self {
669 Self::new(left_field, CompareOp::Lt, right_field)
670 }
671
672 #[must_use]
674 pub fn lte(left_field: String, right_field: String) -> Self {
675 Self::new(left_field, CompareOp::Lte, right_field)
676 }
677
678 #[must_use]
680 pub fn gt(left_field: String, right_field: String) -> Self {
681 Self::new(left_field, CompareOp::Gt, right_field)
682 }
683
684 #[must_use]
686 pub fn gte(left_field: String, right_field: String) -> Self {
687 Self::new(left_field, CompareOp::Gte, right_field)
688 }
689
690 #[must_use]
692 pub fn left_field(&self) -> &str {
693 &self.left_field
694 }
695
696 #[must_use]
698 pub const fn op(&self) -> CompareOp {
699 self.op
700 }
701
702 #[must_use]
704 pub fn right_field(&self) -> &str {
705 &self.right_field
706 }
707
708 #[must_use]
710 pub const fn coercion(&self) -> &CoercionSpec {
711 &self.coercion
712 }
713}
714
715#[cfg_attr(
716 doc,
717 doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
718)]
719#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
720pub enum UnsupportedQueryFeature {
721 #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
722 MapPredicate { field: String },
723}
724
725#[cfg(test)]
730mod tests {
731 use super::*;
732 use crate::db::sql::lowering::PreparedSqlPredicateTemplateShape;
733
734 #[test]
735 fn bind_template_values_rebinds_nested_literal_leaves_without_changing_shape() {
736 let template = Predicate::And(vec![
737 Predicate::Compare(ComparePredicate::with_coercion(
738 "age",
739 CompareOp::Gt,
740 Value::Uint(999),
741 CoercionId::NumericWiden,
742 )),
743 Predicate::Not(Box::new(Predicate::TextContains {
744 field: "name".to_string(),
745 value: Value::Text("__template__".to_string()),
746 })),
747 Predicate::CompareFields(CompareFieldsPredicate::eq(
748 "strength".to_string(),
749 "dexterity".to_string(),
750 )),
751 ]);
752
753 let rebound = template.bind_template_values(&[
754 (Value::Uint(999), Value::Uint(21)),
755 (
756 Value::Text("__template__".to_string()),
757 Value::Text("Ada".to_string()),
758 ),
759 ]);
760
761 assert_eq!(
762 rebound,
763 Predicate::And(vec![
764 Predicate::Compare(ComparePredicate::with_coercion(
765 "age",
766 CompareOp::Gt,
767 Value::Uint(21),
768 CoercionId::NumericWiden,
769 )),
770 Predicate::Not(Box::new(Predicate::TextContains {
771 field: "name".to_string(),
772 value: Value::Text("Ada".to_string()),
773 })),
774 Predicate::CompareFields(CompareFieldsPredicate::eq(
775 "strength".to_string(),
776 "dexterity".to_string(),
777 )),
778 ]),
779 "predicate-owned template rebinding should replace only literal leaves and keep the predicate structure intact",
780 );
781 }
782
783 #[test]
784 fn build_prepared_template_rebinds_compare_slot_owned_literal_leaves() {
785 let predicate = Predicate::Compare(ComparePredicate::with_coercion(
786 "age",
787 CompareOp::Gt,
788 Value::Uint(999),
789 CoercionId::NumericWiden,
790 ));
791
792 let template = predicate
793 .build_prepared_template(PreparedSqlPredicateTemplateShape::CompareWithParamRhs {
794 slot_index: 0,
795 })
796 .expect("prepared predicate template should build");
797 let rebound = template
798 .instantiate(&[Value::Uint(21)])
799 .expect("prepared predicate template should instantiate");
800
801 assert_eq!(
802 rebound,
803 Predicate::Compare(ComparePredicate::with_coercion(
804 "age",
805 CompareOp::Gt,
806 Value::Uint(21),
807 CoercionId::NumericWiden,
808 )),
809 "predicate-owned prepared templates should preserve compare structure and rebind only the slot-owned literal",
810 );
811 }
812}