Repository navigation
Planner Hooks
DataFusion cannot see into a plugin function. So a filter on the function prunes no files and no row groups. A planner hook tells Beacon what a call means. Then Beacon adds plain predicates that the file formats can use. All hooks are optional.
| Builder method | Hook | Use |
|---|---|---|
implies(f) |
FunctionHooks::implied |
A plain predicate that the call implies. |
bounds(f) |
FunctionHooks::bounds |
The output interval for the argument intervals. |
propagate(f) |
FunctionHooks::propagate |
The argument intervals for an output interval. |
Beacon applies the hooks one time per query, after type coercion and before filter pushdown.
example_in_box(lon, lat, min_lon, min_lat, max_lon, max_lat) is true when the point is in the box.
Its implies hook gives a range for each column:
function("example_in_box")
.args(vec![DataType::Float64; 6])
.returns(DataType::Boolean)
.implies(|args| {
let lon = args[0].clone().between(args[2].clone(), args[4].clone());
let lat = args[1].clone().between(args[3].clone(), args[5].clone());
Ok(Some(Implied::Necessary(lon.and(lat))))
})So WHERE example_in_box(lon, lat, 2, 50, 8, 56) also gets lon BETWEEN 2 AND 8 AND lat BETWEEN 50 AND 56.
Beacon uses that predicate to prune files with their statistics.
The hook gets the argument expressions. It returns one of these values:
| Value | Meaning |
|---|---|
Ok(Some(Implied::Necessary(expr))) |
Beacon keeps the call and adds expr next to it. |
Ok(Some(Implied::Exact(expr))) |
Beacon replaces the call with expr. |
Ok(None) |
No rule for these arguments. |
The rule must be correct:
-
Necessary: each row where the call is true must also makeexprtrue. Else rows disappear from the result. -
Exact:exprmust give the same result as the call for each row.
example_add_one(x) gives x + 1. Its hooks work with intervals:
fn one() -> Result<Interval> {
let one = ScalarValue::Float64(Some(1.0));
Interval::try_new(one.clone(), one)
}
function("example_add_one")
// ...
.bounds(|args| Ok(Some(args[0].add(one()?)?)))
.propagate(|output, args| {
// An empty intersection means no row matches. No answer is always correct.
let Some(x) = args[0].intersect(output.sub(one()?)?)? else {
return Ok(None);
};
Ok(Some(vec![x]))
})-
boundsgets the argument intervals and gives the output interval.xin[0, 10]gives[1, 11]. -
propagategets the output interval and the argument intervals. It gives the argument intervals that can make that output. The filterexample_add_one(x) > 10givesx > 9.
Beacon uses propagate for a filter f(..) or f(..) <op> <literal>.
It adds col >= low and col <= high for each column argument.
Beacon uses propagate only when implies gives no rule.
bounds and propagate also answer DataFusion interval analysis, through evaluate_bounds and propagate_constraints.
-
Nested calls. In
example_in_box(example_add_one(lon), lat, 3, 50, 9, 56), Beacon gets the range[3, 9]for the first argument. Then thepropagatehook ofexample_add_onegiveslonin[2, 8]. This also works when the two functions come from different plugins. -
Arithmetic and casts. Beacon follows
+and-with a constant,*and/with a constant for float types, negation and casts. Soexample_add_one(x + 1) > 10givesx > 8. -
Other conditions. A condition such as
x >= 0orx BETWEEN 2 AND 8in the same filter sets the input interval ofx. Apropagatehook gets this interval. So a hook can relate two columns. -
Constants. Beacon folds a constant argument, such as
CAST(2 AS DOUBLE), to a literal beforeimpliessees it. -
Exact rules first. Beacon applies the
Exactrules of calls in the arguments beforeimpliessees them. -
Functions of other plugins. An expression crosses the plugin boundary as
datafusion-protobytes. Beacon decodes it with the functions of the session. So the arguments and the result ofimpliescan call any function that Beacon knows. A function from another library is an FFI wrapper. Find it by its name, not with a downcast.
-
boundsmust hold every value that the function can give. -
propagatemust hold every input value that can give the output. - A wider interval is always correct. A narrower interval that is wrong removes rows from the result.
- A hook that gives
Ok(None), an error or a panic gives no answer. The query does not fail. - Interval arithmetic rounds float bounds outward. So
[0, 10] + 1can give[0.9999999999999999, 11.000000000000002]. Compare intervals with a tolerance in tests.
Implement FunctionHooks for a type. Then wrap the function with HookedUdf::new:
use beacon_plugin_sdk::scalar_functions::{FunctionHooks, HookedUdf};
let udf = ScalarUDF::from(HookedUdf::new(Arc::new(ScalarUDF::from(MyUdf::new())), Arc::new(MyHooks)));FunctionHooks::implied(&self, args, ctx) also gets the session TaskContext. It holds all functions that Beacon knows.
hooks_of(&udf) gives the hooks of a function. Call them directly:
let hooks = hooks_of(&udf).unwrap();
let ctx = SessionContext::new().task_ctx();
let implied = hooks.implied(&args, &ctx).unwrap();
let bounds = hooks.bounds(&[&interval(0.0, 10.0)]).unwrap();
let inputs = hooks.propagate(&interval(5.0, 9.0), &[&interval(0.0, 100.0)]).unwrap();See the tests in native/src/lib.rs and Testing.
For Beacon main at 82839b2 (SDK ABI 6, DataFusion 54). Template repository: maris-development/beacon-plugin-template.