mirror of
https://github.com/quickwit-oss/tantivy.git
synced 2026-10-07 12:22:41 +00:00
Add AND function
This commit is contained in:
@@ -20,7 +20,7 @@ excluded from this pass.
|
||||
|
||||
| Tag | Function | Status |
|
||||
|---:|---|---|
|
||||
| 1 | `AND` | pending |
|
||||
| 1 | `AND` | done |
|
||||
| 2 | `OR` | pending |
|
||||
| 3 | `NOT` | done |
|
||||
| 4 | `ADD` | done |
|
||||
@@ -76,4 +76,4 @@ excluded from this pass.
|
||||
| 79 | `SUBSTRING_COUNT` | pending |
|
||||
| 80 | `REGEXP_LIKE` | pending |
|
||||
|
||||
Progress: **7 / 48 in-scope** functions implemented; **7** functions are out-of-scope.
|
||||
Progress: **8 / 48 in-scope** functions implemented; **7** functions are out-of-scope.
|
||||
|
||||
@@ -118,6 +118,7 @@ fn format_string(value: &str, formatter: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
|
||||
fn function_name(function: Function) -> &'static str {
|
||||
match function {
|
||||
Function::And => "AND",
|
||||
Function::Add => "ADD",
|
||||
Function::Eq => "EQ",
|
||||
Function::IsNull => "IS_NULL",
|
||||
@@ -130,6 +131,7 @@ fn function_name(function: Function) -> &'static str {
|
||||
|
||||
fn parse_function(name: &str, offset: usize) -> Result<Function, DeserializeError> {
|
||||
match name {
|
||||
"AND" => Ok(Function::And),
|
||||
"ADD" => Ok(Function::Add),
|
||||
"EQ" => Ok(Function::Eq),
|
||||
"IS_NULL" => Ok(Function::IsNull),
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
//! `AND` combines one or more nullable boolean expressions.
|
||||
//!
|
||||
//! All arguments must be boolean. With present inputs it is ordinary conjunction. Null handling is
|
||||
//! deliberately stricter than SQL: if any argument is absent, the result is absent even when
|
||||
//! another argument is already `false`. Thus `FALSE AND NULL = NULL`. Null result rows carry a
|
||||
//! false payload, matching dd-go's bitset representation.
|
||||
//!
|
||||
//! dd-go implements this by unioning the operands' null sets and clearing result bits at every null
|
||||
//! row (`interpreter/vector.go`, `propagateNullsBoolean`). The production type checker accepts one
|
||||
//! or more boolean operands. Evaluation order and short-circuiting are query-engine concerns; this
|
||||
//! scalar implementation evaluates every child expression.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cranelift::prelude::{FunctionBuilder, InstBuilder, types};
|
||||
|
||||
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 AndFnCall {
|
||||
pub(crate) args: Box<[TypedExpr]>,
|
||||
}
|
||||
|
||||
impl FnCall for AndFnCall {
|
||||
fn infer_types<'a>(
|
||||
args: &'a [UntypedExpr],
|
||||
target_type: InferredTypeSet,
|
||||
inferred_types: &mut HashMap<&'a str, InferredTypeSet>,
|
||||
) -> Result<InferredTypeSet, TypeError> {
|
||||
if target_type.intersect(InferredTypeSet::BOOLEAN).is_none() {
|
||||
return Err(TypeError::WrongFunctionReturnType {
|
||||
function: Function::And,
|
||||
expected: target_type,
|
||||
got: InferredTypeSet::BOOLEAN,
|
||||
});
|
||||
}
|
||||
if args.is_empty() {
|
||||
return Err(TypeError::InvalidNumberOfArguments {
|
||||
function: Function::And,
|
||||
expected: 1,
|
||||
got: 0,
|
||||
});
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
crate::ast::infer_types_aux(arg, InferredTypeSet::BOOLEAN, inferred_types)?;
|
||||
}
|
||||
Ok(InferredTypeSet::BOOLEAN)
|
||||
}
|
||||
|
||||
fn call_with_types(
|
||||
args: &[UntypedExpr],
|
||||
target_type_set: InferredTypeSet,
|
||||
context: &mut CompileFnBuilder<'_, '_>,
|
||||
) -> Result<TypedExpr, CompileError> {
|
||||
assert!(!args.is_empty(), "expected at least 1 arg for AND");
|
||||
debug_assert!(target_type_set.contains(VarType::Bool));
|
||||
|
||||
let args = args
|
||||
.iter()
|
||||
.map(|arg| context.apply_types(arg, InferredTypeSet::BOOLEAN))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(TypedExpr {
|
||||
return_type: VarType::Bool,
|
||||
ast: TypedExprAst::from_call(AndFnCall {
|
||||
args: args.into_boxed_slice(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
let mut value = builder.ins().iconst(types::I8, 1);
|
||||
let mut is_present = value;
|
||||
|
||||
for arg in &self.args {
|
||||
let lowered = context.compile_expr(arg, builder)?;
|
||||
is_present = builder.ins().band(is_present, lowered.is_present);
|
||||
if arg.return_type == VarType::None {
|
||||
value = builder.ins().iconst(types::I8, 0);
|
||||
} else {
|
||||
value = builder.ins().band(value, lowered.value);
|
||||
}
|
||||
}
|
||||
value = builder.ins().band(value, is_present);
|
||||
|
||||
Ok(LoweredValue {
|
||||
value,
|
||||
is_present,
|
||||
string_len: builder.ins().iconst(types::I64, 0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AndFnCall> for FnCallEnum {
|
||||
fn from(call: AndFnCall) -> Self {
|
||||
FnCallEnum::And(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 runtime inputs and return nullable booleans.
|
||||
unsafe { compiled.call(&[]).as_bool() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infer_types_requires_boolean_arguments() {
|
||||
let expression = deserialize("(AND left right)").unwrap();
|
||||
let inferred_types = infer_types(&expression).unwrap();
|
||||
assert_eq!(inferred_types.get("left"), Some(&InferredTypeSet::BOOLEAN));
|
||||
assert_eq!(inferred_types.get("right"), Some(&InferredTypeSet::BOOLEAN));
|
||||
|
||||
let expression = deserialize("(AND)").unwrap();
|
||||
assert!(matches!(
|
||||
infer_types(&expression),
|
||||
Err(TypeError::InvalidNumberOfArguments {
|
||||
function: Function::And,
|
||||
expected: 1,
|
||||
got: 0,
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_present_truth_table_and_variadic_inputs() {
|
||||
assert_eq!(eval("(AND true)"), Some(true));
|
||||
assert_eq!(eval("(AND true true true)"), Some(true));
|
||||
assert_eq!(eval("(AND true false true)"), Some(false));
|
||||
assert_eq!(eval("(AND false false)"), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_any_absent_argument_makes_result_absent() {
|
||||
assert_eq!(eval("(AND true none)"), None);
|
||||
assert_eq!(eval("(AND false none)"), None);
|
||||
assert_eq!(eval("(AND none none)"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runtime_null_propagation() {
|
||||
let expression = deserialize("(AND left right)").unwrap();
|
||||
let variable_types = HashMap::from([("left", VarType::Bool), ("right", VarType::Bool)]);
|
||||
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(false), VariableValue::none()])
|
||||
.as_bool()
|
||||
},
|
||||
None
|
||||
);
|
||||
// SAFETY: The input and output types match the compiled signature.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
compiled
|
||||
.call(&[VariableValue::some(true), VariableValue::some(false)])
|
||||
.as_bool()
|
||||
},
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod add;
|
||||
mod and;
|
||||
mod eq;
|
||||
mod is_not_null;
|
||||
mod is_null;
|
||||
@@ -12,6 +13,7 @@ use std::collections::HashMap;
|
||||
use cranelift::frontend::FunctionBuilder;
|
||||
|
||||
pub(crate) use self::add::AddFnCall;
|
||||
pub(crate) use self::and::AndFnCall;
|
||||
pub(crate) use self::eq::EqFnCall;
|
||||
pub(crate) use self::is_not_null::IsNotNullFnCall;
|
||||
pub(crate) use self::is_null::IsNullFnCall;
|
||||
@@ -28,6 +30,8 @@ use crate::types::VarType;
|
||||
/// A function supported by the first expression-language milestone.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Function {
|
||||
/// Conjoins one or more booleans with strict null propagation.
|
||||
And,
|
||||
/// Adds zero or more numerical expressions.
|
||||
Add,
|
||||
/// Compares two expressions for value equality.
|
||||
@@ -52,6 +56,7 @@ impl Function {
|
||||
context: &mut CompileFnBuilder<'_, '_>,
|
||||
) -> Result<TypedExpr, CompileError> {
|
||||
match self {
|
||||
Function::And => <AndFnCall as FnCall>::call_with_types(args, target_type_set, context),
|
||||
Function::Add => <AddFnCall as FnCall>::call_with_types(args, target_type_set, context),
|
||||
Function::Eq => <EqFnCall as FnCall>::call_with_types(args, target_type_set, context),
|
||||
Function::IsNotNull => {
|
||||
@@ -77,6 +82,7 @@ impl Function {
|
||||
inferred_types: &mut HashMap<&'a str, InferredTypeSet>,
|
||||
) -> Result<InferredTypeSet, TypeError> {
|
||||
match self {
|
||||
Function::And => <AndFnCall as FnCall>::infer_types(args, target_type, inferred_types),
|
||||
Function::Add => <AddFnCall as FnCall>::infer_types(args, target_type, inferred_types),
|
||||
Function::Eq => <EqFnCall as FnCall>::infer_types(args, target_type, inferred_types),
|
||||
Function::IsNotNull => {
|
||||
@@ -105,6 +111,7 @@ impl Function {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum FnCallEnum {
|
||||
And(AndFnCall),
|
||||
Add(AddFnCall),
|
||||
Eq(EqFnCall),
|
||||
IsNull(IsNullFnCall),
|
||||
@@ -117,6 +124,7 @@ pub(crate) enum FnCallEnum {
|
||||
impl FnCallEnum {
|
||||
pub(crate) fn args_mut(&mut self) -> &mut [TypedExpr] {
|
||||
match self {
|
||||
FnCallEnum::And(call) => call.args_mut(),
|
||||
FnCallEnum::Add(call) => call.args_mut(),
|
||||
FnCallEnum::Eq(call) => call.args_mut(),
|
||||
FnCallEnum::IsNull(call) => call.args_mut(),
|
||||
@@ -135,6 +143,7 @@ impl FnCallEnum {
|
||||
builder: &mut FunctionBuilder<'_>,
|
||||
) -> Result<LoweredValue, CompileError> {
|
||||
match self {
|
||||
FnCallEnum::And(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::Add(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::Eq(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
FnCallEnum::IsNull(call) => call.emit_cranelift_ir(return_type, context, builder),
|
||||
|
||||
Reference in New Issue
Block a user