use crate::{
db::{
predicate::{
CoercionId, CompareFieldsPredicate, CompareOp, ComparePredicate, MembershipCompareLeaf,
Predicate, collapse_membership_compare_leaves,
},
query::plan::expr::{
BinaryOp, BooleanFunctionShape, CanonicalExpr, CaseWhenArm, Expr,
FieldPredicateFunctionKind, Function, NullTestFunctionKind, TextPredicateFunctionKind,
UnaryOp, truth_condition_binary_compare_op,
},
},
value::Value,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct PredicateCompilation {
predicate: Predicate,
}
impl PredicateCompilation {
const fn new(predicate: Predicate) -> Self {
Self { predicate }
}
pub(in crate::db) fn into_predicate(self) -> Predicate {
self.predicate
}
}
#[must_use]
pub(in crate::db) fn compile_canonical_bool_expr_to_compiled_predicate(
expr: &CanonicalExpr,
) -> PredicateCompilation {
PredicateCompilation::new(compile_normalized_bool_expr_to_predicate_impl(
expr.as_expr(),
))
}
#[must_use]
#[cfg(test)]
pub(in crate::db) fn compile_normalized_bool_expr_to_predicate(expr: &Expr) -> Predicate {
let canonical = CanonicalExpr::from_normalized_bool_expr(expr)
.expect("predicate compilation requires normalized boolean expression canonical shape");
compile_canonical_bool_expr_to_compiled_predicate(&canonical).into_predicate()
}
fn compile_normalized_bool_expr_to_predicate_impl(expr: &Expr) -> Predicate {
debug_assert!(
runtime_predicate_admissible_expr(expr),
"normalized boolean expression"
);
if let Some(predicate) = collapse_membership_bool_expr(expr) {
return crate::db::predicate::normalize(&predicate);
}
crate::db::predicate::normalize(&compile_bool_truth_sets(expr).0)
}
#[must_use]
pub(in crate::db) fn derive_canonical_bool_expr_predicate_subset(
expr: &CanonicalExpr,
) -> Option<Predicate> {
runtime_predicate_admissible_expr(expr.as_expr())
.then(|| compile_canonical_bool_expr_to_compiled_predicate(expr).into_predicate())
}
#[must_use]
pub(in crate::db) fn derive_normalized_bool_expr_predicate_subset(
expr: &Expr,
) -> Option<Predicate> {
let canonical = CanonicalExpr::from_normalized_bool_expr(expr)?;
derive_canonical_bool_expr_predicate_subset(&canonical)
}
fn collapse_membership_bool_expr(expr: &Expr) -> Option<Predicate> {
match expr {
Expr::Binary {
op: BinaryOp::Or, ..
} => collapse_same_field_compare_chain(expr, BinaryOp::Or, BinaryOp::Eq, CompareOp::In),
Expr::Binary {
op: BinaryOp::And, ..
} => collapse_same_field_compare_chain(expr, BinaryOp::And, BinaryOp::Ne, CompareOp::NotIn),
Expr::Field(_)
| Expr::FieldPath(_)
| Expr::Literal(_)
| Expr::Unary { .. }
| Expr::Aggregate(_)
| Expr::FunctionCall { .. }
| Expr::Case { .. }
| Expr::Binary { .. } => None,
#[cfg(test)]
Expr::Alias { .. } => None,
}
}
fn collapse_same_field_compare_chain(
expr: &Expr,
join_op: BinaryOp,
compare_op: BinaryOp,
target_op: CompareOp,
) -> Option<Predicate> {
let mut leaves = Vec::new();
collect_compare_chain(expr, join_op, &mut leaves)?;
let mut membership_leaves = Vec::with_capacity(leaves.len());
for leaf in leaves {
let (leaf_field, leaf_value, leaf_coercion) = membership_compare_leaf(leaf, compare_op)?;
membership_leaves.push((leaf_field, leaf_value, leaf_coercion));
}
let (_, _, first_coercion) = membership_leaves.first()?;
if !membership_leaves
.iter()
.all(|(_, _, coercion)| coercion == first_coercion)
{
return None;
}
collapse_membership_compare_leaves(
membership_leaves
.into_iter()
.map(|(field, value, coercion)| MembershipCompareLeaf::new(field, value, coercion)),
target_op,
)
.map(Predicate::Compare)
}
fn collect_compare_chain<'a>(
expr: &'a Expr,
join_op: BinaryOp,
out: &mut Vec<&'a Expr>,
) -> Option<()> {
match expr {
Expr::Binary { op, left, right } if *op == join_op => {
collect_compare_chain(left.as_ref(), join_op, out)?;
collect_compare_chain(right.as_ref(), join_op, out)
}
Expr::Binary { .. } => {
out.push(expr);
Some(())
}
Expr::Field(_)
| Expr::FieldPath(_)
| Expr::Literal(_)
| Expr::Unary { .. }
| Expr::Aggregate(_)
| Expr::FunctionCall { .. }
| Expr::Case { .. } => None,
#[cfg(test)]
Expr::Alias { .. } => None,
}
}
fn membership_compare_leaf(expr: &Expr, compare_op: BinaryOp) -> Option<(&str, Value, CoercionId)> {
let Expr::Binary { op, left, right } = expr else {
return None;
};
if *op != compare_op {
return None;
}
match (left.as_ref(), right.as_ref()) {
(Expr::Field(field), Expr::Literal(value)) if membership_value_is_in_safe(value) => Some((
field.as_str(),
value.clone(),
compare_literal_coercion(
truth_condition_binary_compare_op(*op)
.expect("compile-ready compare operands must resolve onto CompareOp"),
value,
),
)),
(
Expr::FunctionCall {
function: Function::Lower,
args,
},
Expr::Literal(Value::Text(value)),
) => match args.as_slice() {
[Expr::Field(field)] => Some((
field.as_str(),
Value::Text(value.clone()),
CoercionId::TextCasefold,
)),
_ => None,
},
_ => None,
}
}
const fn membership_value_is_in_safe(value: &Value) -> bool {
!matches!(value, Value::List(_) | Value::Map(_))
}
fn compile_bool_truth_sets(expr: &Expr) -> (Predicate, Predicate) {
debug_assert!(runtime_predicate_admissible_expr(expr));
match expr {
Expr::Field(field) => compile_bool_field_truth_sets(field.as_str()),
Expr::Literal(Value::Bool(true)) => (Predicate::True, Predicate::False),
Expr::Literal(Value::Bool(false)) => (Predicate::False, Predicate::True),
Expr::Literal(Value::Null) => (Predicate::False, Predicate::False),
Expr::Literal(_) => {
unreachable!("boolean compilation expects only boolean-context literals")
}
Expr::FieldPath(_) => {
unreachable!("boolean compilation expects compile-ready field leaves")
}
Expr::Unary {
op: UnaryOp::Not,
expr,
} => {
let (when_true, when_false) = compile_bool_truth_sets(expr.as_ref());
(when_false, when_true)
}
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => {
let (left_true, left_false) = compile_bool_truth_sets(left.as_ref());
let (right_true, right_false) = compile_bool_truth_sets(right.as_ref());
(
Predicate::And(vec![left_true, right_true]),
Predicate::Or(vec![left_false, right_false]),
)
}
Expr::Binary {
op: BinaryOp::Or,
left,
right,
} => {
let (left_true, left_false) = compile_bool_truth_sets(left.as_ref());
let (right_true, right_false) = compile_bool_truth_sets(right.as_ref());
(
Predicate::Or(vec![left_true, right_true]),
Predicate::And(vec![left_false, right_false]),
)
}
Expr::Binary { op, left, right } => {
compile_bool_compare_truth_sets(*op, left.as_ref(), right.as_ref())
}
Expr::FunctionCall { function, args } => compile_bool_function_truth_sets(*function, args),
Expr::Case {
when_then_arms,
else_expr,
} => compile_bool_case_truth_sets(when_then_arms.as_slice(), else_expr.as_ref()),
Expr::Aggregate(_) => {
unreachable!("boolean compilation expects boolean-only expression shapes")
}
#[cfg(test)]
Expr::Alias { .. } => {
unreachable!("boolean compilation should never receive alias wrappers")
}
}
}
fn compile_bool_case_truth_sets(arms: &[CaseWhenArm], else_expr: &Expr) -> (Predicate, Predicate) {
let (mut residual_true, mut residual_false) = compile_bool_truth_sets(else_expr);
for arm in arms.iter().rev() {
let (condition_true, _) = compile_bool_truth_sets(arm.condition());
let (result_true, result_false) = compile_bool_truth_sets(arm.result());
let skipped = Predicate::Not(Box::new(condition_true.clone()));
residual_true = Predicate::Or(vec![
Predicate::And(vec![condition_true.clone(), result_true]),
Predicate::And(vec![skipped.clone(), residual_true]),
]);
residual_false = Predicate::Or(vec![
Predicate::And(vec![condition_true, result_false]),
Predicate::And(vec![skipped, residual_false]),
]);
}
(residual_true, residual_false)
}
fn compile_bool_field_truth_sets(field: &str) -> (Predicate, Predicate) {
let when_true = Predicate::Compare(ComparePredicate::with_coercion(
field.to_string(),
CompareOp::Eq,
Value::Bool(true),
CoercionId::Strict,
));
let when_false = Predicate::Compare(ComparePredicate::with_coercion(
field.to_string(),
CompareOp::Eq,
Value::Bool(false),
CoercionId::Strict,
));
(when_true, when_false)
}
fn compile_bool_compare_truth_sets(
op: BinaryOp,
left: &Expr,
right: &Expr,
) -> (Predicate, Predicate) {
if matches!(left, Expr::Literal(Value::Null)) || matches!(right, Expr::Literal(Value::Null)) {
return (Predicate::False, Predicate::False);
}
let when_true = compile_bool_compare_expr(op, left, right);
(when_true.clone(), Predicate::Not(Box::new(when_true)))
}
fn compile_bool_compare_expr(op: BinaryOp, left: &Expr, right: &Expr) -> Predicate {
let op = truth_condition_binary_compare_op(op)
.expect("compile-ready binary compare operators must lower onto CompareOp");
match (left, right) {
(Expr::Field(field), Expr::Literal(value)) => {
Predicate::Compare(ComparePredicate::with_coercion(
field.as_str().to_string(),
op,
value.clone(),
compare_literal_coercion(op, value),
))
}
(Expr::Literal(value), Expr::Field(field)) => {
Predicate::Compare(ComparePredicate::with_coercion(
field.as_str().to_string(),
op.flipped(),
value.clone(),
compare_literal_coercion(op.flipped(), value),
))
}
(Expr::Field(left_field), Expr::Field(right_field)) => {
Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
left_field.as_str().to_string(),
op,
right_field.as_str().to_string(),
compare_field_coercion(op),
))
}
(
Expr::FunctionCall {
function: Function::Lower,
args,
},
Expr::Literal(Value::Text(value)),
) => match args.as_slice() {
[Expr::Field(field)] => Predicate::Compare(ComparePredicate::with_coercion(
field.as_str().to_string(),
op,
Value::Text(value.clone()),
CoercionId::TextCasefold,
)),
_ => unreachable!("boolean compilation expects LOWER(field) compare wrappers"),
},
_ => unreachable!("boolean compilation expects canonical compare operands"),
}
}
fn compile_bool_function_truth_sets(function: Function, args: &[Expr]) -> (Predicate, Predicate) {
match function.boolean_function_shape() {
Some(BooleanFunctionShape::NullTest) => compile_bool_null_test_function_truth_sets(
function
.boolean_null_test_kind()
.expect("null-test boolean family must keep one null-test kind"),
args,
),
Some(BooleanFunctionShape::TextPredicate) => {
let kind = boolean_text_predicate_kind(function);
match kind {
TextPredicateFunctionKind::StartsWith | TextPredicateFunctionKind::EndsWith => {
compile_bool_prefix_text_function_truth_sets(kind, args)
}
TextPredicateFunctionKind::Contains => {
compile_bool_contains_function_truth_sets(args)
}
}
}
Some(BooleanFunctionShape::FieldPredicate) => match function
.boolean_field_predicate_kind()
.expect("field-predicate boolean family must keep one field-predicate kind")
{
FieldPredicateFunctionKind::Missing => {
compile_bool_field_predicate_truth_sets(args, |field| Predicate::IsMissing {
field: field.to_string(),
})
}
FieldPredicateFunctionKind::Empty => {
compile_bool_field_predicate_truth_sets(args, |field| Predicate::IsEmpty {
field: field.to_string(),
})
}
FieldPredicateFunctionKind::NotEmpty => {
compile_bool_field_predicate_truth_sets(args, |field| Predicate::IsNotEmpty {
field: field.to_string(),
})
}
},
Some(BooleanFunctionShape::CollectionContains) => {
compile_bool_collection_contains_truth_sets(args)
}
Some(BooleanFunctionShape::TruthCoalesce) | None => {
unreachable!("boolean compilation expects only directly compilable boolean functions")
}
}
}
const fn boolean_text_predicate_kind(function: Function) -> TextPredicateFunctionKind {
function
.boolean_text_predicate_kind()
.expect("text-predicate boolean family must keep one text-predicate kind")
}
fn compile_bool_null_test_function_truth_sets(
kind: NullTestFunctionKind,
args: &[Expr],
) -> (Predicate, Predicate) {
let [arg] = args else {
unreachable!("boolean null tests keep one operand")
};
match arg {
Expr::Field(field) => {
let is_null = Predicate::IsNull {
field: field.as_str().to_string(),
};
let is_not_null = Predicate::IsNotNull {
field: field.as_str().to_string(),
};
if kind.null_matches_true() {
(is_null, is_not_null)
} else {
(is_not_null, is_null)
}
}
Expr::Literal(value) => {
let literal_is_true = kind.null_matches_true() == matches!(value, Value::Null);
constant_bool_truth_sets(literal_is_true)
}
_ => unreachable!("boolean null tests expect field/literal operands"),
}
}
const fn constant_bool_truth_sets(value: bool) -> (Predicate, Predicate) {
if value {
(Predicate::True, Predicate::False)
} else {
(Predicate::False, Predicate::True)
}
}
fn compile_bool_prefix_text_function_truth_sets(
kind: TextPredicateFunctionKind,
args: &[Expr],
) -> (Predicate, Predicate) {
let [left, Expr::Literal(Value::Text(value))] = args else {
unreachable!("boolean prefix text predicates keep field/text operands")
};
let (field, coercion) = compile_bool_text_target(left);
let op = match kind {
TextPredicateFunctionKind::StartsWith => CompareOp::StartsWith,
TextPredicateFunctionKind::EndsWith => CompareOp::EndsWith,
TextPredicateFunctionKind::Contains => {
unreachable!("prefix compiler called with non-prefix text-predicate kind")
}
};
let when_true = Predicate::Compare(ComparePredicate::with_coercion(
field,
op,
Value::Text(value.clone()),
coercion,
));
(when_true.clone(), Predicate::Not(Box::new(when_true)))
}
fn compile_bool_contains_function_truth_sets(args: &[Expr]) -> (Predicate, Predicate) {
let [left, Expr::Literal(Value::Text(value))] = args else {
unreachable!("boolean contains predicates keep field/text operands")
};
let (field, coercion) = compile_bool_text_target(left);
let when_true = match coercion {
CoercionId::Strict => Predicate::TextContains {
field,
value: Value::Text(value.clone()),
},
CoercionId::TextCasefold => Predicate::TextContainsCi {
field,
value: Value::Text(value.clone()),
},
CoercionId::NumericWiden | CoercionId::CollectionElement => {
unreachable!("boolean contains predicates only compile text coercions");
}
};
(when_true.clone(), Predicate::Not(Box::new(when_true)))
}
fn compile_bool_field_predicate_truth_sets(
args: &[Expr],
build: impl FnOnce(&str) -> Predicate,
) -> (Predicate, Predicate) {
let [Expr::Field(field)] = args else {
unreachable!("field-only boolean function expects one field argument")
};
let when_true = build(field.as_str());
(when_true.clone(), Predicate::Not(Box::new(when_true)))
}
fn compile_bool_collection_contains_truth_sets(args: &[Expr]) -> (Predicate, Predicate) {
let [Expr::Field(field), Expr::Literal(value)] = args else {
unreachable!("collection contains expects field/literal operands")
};
let when_true = Predicate::Compare(ComparePredicate::with_coercion(
field.as_str().to_string(),
CompareOp::Contains,
value.clone(),
CoercionId::Strict,
));
(when_true.clone(), Predicate::Not(Box::new(when_true)))
}
fn compile_bool_text_target(expr: &Expr) -> (String, CoercionId) {
match expr {
Expr::Field(field) => (field.as_str().to_string(), CoercionId::Strict),
Expr::FunctionCall {
function: Function::Lower,
args,
} => match args.as_slice() {
[Expr::Field(field)] => (field.as_str().to_string(), CoercionId::TextCasefold),
_ => unreachable!("boolean text targets only compile LOWER(field) wrappers"),
},
_ => unreachable!("boolean text targets only compile canonical field wrappers"),
}
}
fn runtime_predicate_admissible_expr(expr: &Expr) -> bool {
RuntimePredicateAdmission::is_admissible(expr)
}
struct RuntimePredicateAdmission;
impl RuntimePredicateAdmission {
fn is_admissible(expr: &Expr) -> bool {
match expr {
Expr::Field(_) => true,
Expr::Literal(Value::Bool(_) | Value::Null) => true,
Expr::Unary {
op: UnaryOp::Not,
expr,
} => {
!matches!(
expr.as_ref(),
Expr::Unary {
op: UnaryOp::Not,
..
}
) && Self::is_admissible(expr.as_ref())
}
Expr::Binary {
op: BinaryOp::And | BinaryOp::Or,
left,
right,
} => Self::is_admissible(left.as_ref()) && Self::is_admissible(right.as_ref()),
Expr::Binary { op, left, right } => Self::is_compare_expr(*op, left, right),
Expr::FunctionCall { function, args } => {
Self::is_bool_function_call(*function, args.as_slice())
}
Expr::Case {
when_then_arms,
else_expr,
} => {
when_then_arms.iter().all(|arm| {
Self::is_admissible(arm.condition()) && Self::is_admissible(arm.result())
}) && Self::is_admissible(else_expr.as_ref())
}
Expr::FieldPath(_) | Expr::Aggregate(_) | Expr::Literal(_) => false,
#[cfg(test)]
Expr::Alias { .. } => false,
}
}
fn is_compare_expr(op: BinaryOp, left: &Expr, right: &Expr) -> bool {
if truth_condition_binary_compare_op(op).is_none() {
return false;
}
match (left, right) {
(Expr::Field(_), Expr::Literal(_) | Expr::Field(_)) => true,
(
Expr::FunctionCall {
function: Function::Lower,
args,
},
Expr::Literal(Value::Text(_)),
) => matches!(args.as_slice(), [Expr::Field(_)]),
_ => false,
}
}
fn is_bool_function_call(function: Function, args: &[Expr]) -> bool {
match function.boolean_function_shape() {
Some(BooleanFunctionShape::NullTest) => {
matches!(args, [Expr::Field(_) | Expr::Literal(_)])
}
Some(BooleanFunctionShape::TextPredicate) => {
matches!(args, [left, Expr::Literal(Value::Text(_))] if Self::is_text_target(left))
}
Some(BooleanFunctionShape::FieldPredicate) => {
matches!(args, [Expr::Field(_)])
}
Some(BooleanFunctionShape::CollectionContains) => {
matches!(args, [Expr::Field(_), Expr::Literal(_)])
}
Some(BooleanFunctionShape::TruthCoalesce) | None => false,
}
}
fn is_text_target(expr: &Expr) -> bool {
match expr {
Expr::Field(_) => true,
Expr::FunctionCall {
function: Function::Lower,
args,
} => matches!(args.as_slice(), [Expr::Field(_)]),
_ => false,
}
}
}
const fn compare_literal_coercion(op: CompareOp, value: &Value) -> CoercionId {
match value {
Value::Text(_) | Value::Uint(_) | Value::Uint128(_) | Value::UintBig(_) => {
CoercionId::Strict
}
Value::Float32(_) | Value::Float64(_) | Value::Decimal(_) => {
if op.is_ordering_family() {
CoercionId::NumericWiden
} else {
CoercionId::Strict
}
}
_ if value.supports_numeric_coercion() => CoercionId::NumericWiden,
_ => CoercionId::Strict,
}
}
fn compare_field_coercion(op: CompareOp) -> CoercionId {
if !op.supports_field_compare() {
unreachable!("non-field compare operator cannot lower to CompareFieldsPredicate");
}
if op.is_ordering_family() {
CoercionId::NumericWiden
} else {
CoercionId::Strict
}
}