mirror of
https://github.com/quickwit-oss/tantivy.git
synced 2026-10-06 20:02:45 +00:00
Add GT_EQ function
This commit is contained in:
@@ -30,7 +30,7 @@ excluded from this pass.
|
||||
| 8 | `EQ` | done |
|
||||
| 9 | `GT` | done |
|
||||
| 10 | `LT` | done |
|
||||
| 11 | `GT_EQ` | pending |
|
||||
| 11 | `GT_EQ` | done |
|
||||
| 12 | `LT_EQ` | pending |
|
||||
| 13 | `IS_NULL` | done |
|
||||
| 14 | `IS_NOT_NULL` | done |
|
||||
@@ -76,4 +76,4 @@ excluded from this pass.
|
||||
| 79 | `SUBSTRING_COUNT` | pending |
|
||||
| 80 | `REGEXP_LIKE` | pending |
|
||||
|
||||
Progress: **14 / 48 in-scope** functions implemented; **7** functions are out-of-scope.
|
||||
Progress: **15 / 48 in-scope** functions implemented; **7** functions are out-of-scope.
|
||||
|
||||
@@ -123,6 +123,7 @@ fn function_name(function: Function) -> &'static str {
|
||||
Function::Divide => "DIVIDE",
|
||||
Function::Eq => "EQ",
|
||||
Function::Gt => "GT",
|
||||
Function::GtEq => "GT_EQ",
|
||||
Function::Lt => "LT",
|
||||
Function::IsNull => "IS_NULL",
|
||||
Function::IsNotNull => "IS_NOT_NULL",
|
||||
@@ -142,6 +143,7 @@ fn parse_function(name: &str, offset: usize) -> Result<Function, DeserializeErro
|
||||
"DIVIDE" => Ok(Function::Divide),
|
||||
"EQ" => Ok(Function::Eq),
|
||||
"GT" => Ok(Function::Gt),
|
||||
"GT_EQ" => Ok(Function::GtEq),
|
||||
"LT" => Ok(Function::Lt),
|
||||
"IS_NULL" => Ok(Function::IsNull),
|
||||
"IS_NOT_NULL" => Ok(Function::IsNotNull),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! `GT_EQ` tests whether one scalar value is greater than or equal to another.
|
||||
//!
|
||||
//! It accepts exactly two operands. Ordered production operands are strings or numbers; booleans
|
||||
//! are rejected. Strings use lexicographic UTF-8 ordering. Numeric comparisons support `i64`,
|
||||
//! `u64`, and `f64` combinations without converting large integers through a lossy `f64`. IEEE
|
||||
//! unordered comparisons involving NaN return `false`, including `NaN >= NaN`.
|
||||
//!
|
||||
//! Null propagation is strict: if either operand is absent, the result is absent. This follows
|
||||
//! dd-go's predicate path (`arrayCOMPARE` followed by `propagateNullsPredicateLazy`). Multivalued
|
||||
//! comparison uses existential semantics in dd-go but is outside jitexpr's scalar type model.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cranelift::frontend::FunctionBuilder;
|
||||
|
||||
use super::comparison::{self, OrderedComparison};
|
||||
use crate::ast::{Function, InferredTypeSet, TypeError, UntypedExpr};
|
||||
use crate::compile::{
|
||||
CompileError, CompileFnBuilder, LoweredValue, LoweringContext, TypedExpr, TypedExprAst,
|
||||
};
|
||||
use crate::functions::{FnCall, FnCallEnum};
|
||||
use crate::types::VarType;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct GtEqFnCall {
|
||||
pub(crate) args: Box<[TypedExpr]>,
|
||||
}
|
||||
|
||||
impl FnCall for GtEqFnCall {
|
||||
fn infer_types<'a>(
|
||||
args: &'a [UntypedExpr],
|
||||
target_type: InferredTypeSet,
|
||||
inferred_types: &mut HashMap<&'a str, InferredTypeSet>,
|
||||
) -> Result<InferredTypeSet, TypeError> {
|
||||
comparison::infer_types(Function::GtEq, args, target_type, inferred_types)
|
||||
}
|
||||
|
||||
fn call_with_types(
|
||||
args: &[UntypedExpr],
|
||||
target_type_set: InferredTypeSet,
|
||||
context: &mut CompileFnBuilder<'_, '_>,
|
||||
) -> Result<TypedExpr, CompileError> {
|
||||
assert_eq!(args.len(), 2, "expected 2 args for GT_EQ");
|
||||
debug_assert!(target_type_set.contains(VarType::Bool));
|
||||
Ok(TypedExpr {
|
||||
return_type: VarType::Bool,
|
||||
ast: TypedExprAst::from_call(GtEqFnCall {
|
||||
args: comparison::apply_types(args, context)?,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn args_mut(&mut self) -> &mut [TypedExpr] {
|
||||
&mut self.args
|
||||
}
|
||||
|
||||
fn emit_cranelift_ir(
|
||||
&self,
|
||||
return_type: VarType,
|
||||
context: &mut LoweringContext<'_>,
|
||||
builder: &mut FunctionBuilder<'_>,
|
||||
) -> Result<LoweredValue, CompileError> {
|
||||
debug_assert_eq!(return_type, VarType::Bool);
|
||||
comparison::lower(
|
||||
&self.args,
|
||||
OrderedComparison::GreaterThanOrEqual,
|
||||
context,
|
||||
builder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GtEqFnCall> for FnCallEnum {
|
||||
fn from(call: GtEqFnCall) -> Self {
|
||||
FnCallEnum::GtEq(call)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{deserialize, infer_types};
|
||||
use crate::compile::compile;
|
||||
use crate::types::VariableValue;
|
||||
|
||||
fn eval(expression: &str) -> Option<bool> {
|
||||
let expression = deserialize(expression).unwrap();
|
||||
let mut compiled = compile(&expression, &HashMap::new()).unwrap();
|
||||
// SAFETY: These expressions have no inputs and return nullable booleans.
|
||||
unsafe { compiled.call(&[]).as_bool() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_two_ordered_arguments() {
|
||||
let expression = deserialize("(GT_EQ left right)").unwrap();
|
||||
let inferred_types = infer_types(&expression).unwrap();
|
||||
let left = inferred_types.get("left").unwrap();
|
||||
assert!(left.string && left.i64 && left.u64 && left.f64 && !left.boolean);
|
||||
|
||||
let expression = deserialize("(GT_EQ 1i64 2i64 3i64)").unwrap();
|
||||
assert!(matches!(
|
||||
infer_types(&expression),
|
||||
Err(TypeError::InvalidNumberOfArguments {
|
||||
function: Function::GtEq,
|
||||
expected: 2,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ordering_equality_nan_and_null() {
|
||||
assert_eq!(eval(r#"(GT_EQ "same" "same")"#), Some(true));
|
||||
assert_eq!(eval(r#"(GT_EQ "alpha" "beta")"#), Some(false));
|
||||
assert_eq!(eval("(GT_EQ 0u64 -1i64)"), Some(true));
|
||||
assert_eq!(
|
||||
eval("(GT_EQ 9007199254740993u64 9007199254740992f64)"),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(eval("(GT_EQ nanf64 nanf64)"), Some(false));
|
||||
assert_eq!(eval("(GT_EQ nanf64 0i64)"), Some(false));
|
||||
assert_eq!(eval("(GT_EQ 0i64 nanf64)"), Some(false));
|
||||
assert_eq!(eval("(GT_EQ none 1i64)"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runtime_null_propagates() {
|
||||
let expression = deserialize("(GT_EQ left right)").unwrap();
|
||||
let variable_types = HashMap::from([("left", VarType::Str), ("right", VarType::Str)]);
|
||||
let mut compiled = compile(&expression, &variable_types).unwrap();
|
||||
// SAFETY: The input and output types match the compiled signature.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
compiled
|
||||
.call(&[VariableValue::some("a"), VariableValue::none()])
|
||||
.as_bool()
|
||||
},
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod comparison;
|
||||
mod divide;
|
||||
mod eq;
|
||||
mod gt;
|
||||
mod gt_eq;
|
||||
mod is_not_null;
|
||||
mod is_null;
|
||||
mod lower;
|
||||
@@ -24,6 +25,7 @@ pub(crate) use self::and::AndFnCall;
|
||||
pub(crate) use self::divide::DivideFnCall;
|
||||
pub(crate) use self::eq::EqFnCall;
|
||||
pub(crate) use self::gt::GtFnCall;
|
||||
pub(crate) use self::gt_eq::GtEqFnCall;
|
||||
pub(crate) use self::is_not_null::IsNotNullFnCall;
|
||||
pub(crate) use self::is_null::IsNullFnCall;
|
||||
pub(crate) use self::lower::LowerFnCall;
|
||||
@@ -53,6 +55,8 @@ pub enum Function {
|
||||
Eq,
|
||||
/// Tests whether the first ordered value is greater than the second.
|
||||
Gt,
|
||||
/// Tests whether the first ordered value is greater than or equal to the second.
|
||||
GtEq,
|
||||
/// Tests whether the first ordered value is less than the second.
|
||||
Lt,
|
||||
/// Tests whether an expression produced a present value.
|
||||
@@ -88,6 +92,9 @@ impl Function {
|
||||
}
|
||||
Function::Eq => <EqFnCall as FnCall>::call_with_types(args, target_type_set, context),
|
||||
Function::Gt => <GtFnCall as FnCall>::call_with_types(args, target_type_set, context),
|
||||
Function::GtEq => {
|
||||
<GtEqFnCall as FnCall>::call_with_types(args, target_type_set, context)
|
||||
}
|
||||
Function::Lt => <LtFnCall as FnCall>::call_with_types(args, target_type_set, context),
|
||||
Function::IsNotNull => {
|
||||
<IsNotNullFnCall as FnCall>::call_with_types(args, target_type_set, context)
|
||||
@@ -126,6 +133,9 @@ impl Function {
|
||||
}
|
||||
Function::Eq => <EqFnCall as FnCall>::infer_types(args, target_type, inferred_types),
|
||||
Function::Gt => <GtFnCall as FnCall>::infer_types(args, target_type, inferred_types),
|
||||
Function::GtEq => {
|
||||
<GtEqFnCall as FnCall>::infer_types(args, target_type, inferred_types)
|
||||
}
|
||||
Function::Lt => <LtFnCall as FnCall>::infer_types(args, target_type, inferred_types),
|
||||
Function::IsNotNull => {
|
||||
<IsNotNullFnCall as FnCall>::infer_types(args, target_type, inferred_types)
|
||||
@@ -165,6 +175,7 @@ pub(crate) enum FnCallEnum {
|
||||
Divide(DivideFnCall),
|
||||
Eq(EqFnCall),
|
||||
Gt(GtFnCall),
|
||||
GtEq(GtEqFnCall),
|
||||
Lt(LtFnCall),
|
||||
IsNull(IsNullFnCall),
|
||||
IsNotNull(IsNotNullFnCall),
|
||||
@@ -184,6 +195,7 @@ impl FnCallEnum {
|
||||
FnCallEnum::Divide(call) => call.args_mut(),
|
||||
FnCallEnum::Eq(call) => call.args_mut(),
|
||||
FnCallEnum::Gt(call) => call.args_mut(),
|
||||
FnCallEnum::GtEq(call) => call.args_mut(),
|
||||
FnCallEnum::Lt(call) => call.args_mut(),
|
||||
FnCallEnum::IsNull(call) => call.args_mut(),
|
||||
FnCallEnum::IsNotNull(call) => call.args_mut(),
|
||||
@@ -209,6 +221,7 @@ impl FnCallEnum {
|
||||
FnCallEnum::Divide(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::Eq(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::Gt(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::GtEq(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::Lt(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::IsNull(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::IsNotNull(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
|
||||
Reference in New Issue
Block a user