Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Checkpoint 9: Bind Logical Calls to Physical Expressions

Checkpoint 8 accepts a physical function and exact physical input types. A query planner begins one level earlier, with a logical function name and logical input types such as SmallInt, Integer, Char, or Varchar. In this checkpoint, you will connect those two worlds with one binding layer.

Begin from completed Checkpoint 8 and copy the cumulative tests:

cargo x copy-test --chapter 9
cargo test -p type-exercise-starter-supplied-tests chapter_9 --locked

The focused run should fail only because the Checkpoint 9 logical-call, bound-expression, and binder names are missing.

Describe one logical call

In expr/src/binder.rs, implement this public surface:

pub struct LogicalCall { /* logical name and input DataTypes */ }

impl LogicalCall {
    pub fn new(
        name: impl Into<String>,
        input_types: impl IntoIterator<Item = DataType>,
    ) -> Self;
    pub fn name(&self) -> &str;
    pub fn input_types(&self) -> &[DataType];
}

pub enum BindError { /* unknown, wrong arity, unsupported, metadata mismatch */ }

pub struct BoundExpression { /* logical contract plus Box<dyn Expression> */ }

pub fn bind_logical_call(call: LogicalCall) -> Result<BoundExpression, BindError>;

BoundExpression should expose the logical call and output type, a borrowed view of the selected physical expression, a way to take its Box<dyn Expression>, and an evaluate method that delegates the entire batch. Its constructor must reject logical metadata whose physical input or output types disagree with the expression it wraps.

Enable the binder module from expr/src/lib.rs. Logical binding belongs in the facade. Core continues to own arrays, logical and physical representations, validation, generic traversal, writers, and the erased batch boundary.

Resolve only the maintained overloads

Map these logical names to the physical catalog identifiers already earned in Checkpoint 8:

Logical namesAccepted logical inputsLogical output
+, -, *, /two losslessly compatible numeric typestheir promoted numeric type
negone supported numeric typethe input type
clampthree pairwise-promotable numeric typesthe final promoted type
<, <=, >, >=compatible numeric types, or two string typesBoolean
=, !=the comparison cases above, or two BooleansBoolean
boolean_and, boolean_ortwo BooleansBoolean
boolean_notone BooleanBoolean
concatany Char/Varchar pairVarchar
containsany Char/Varchar pairBoolean

Use the same lossless numeric policy as the physical catalog. SmallInt widens to every maintained numeric family. Integer combines with BigInt or Double; Integer plus Real produces Double. Real combines with Double. Reject BigInt with either floating family, Decimal overloads not represented by the physical catalog, and every other unsupported combination.

Char and Varchar are distinct logical types but both map to physical String. That mapping is why a mixed Char/Varchar concat call selects the existing StringConcat expression without a new row loop or a runtime cast.

Check arity before overload resolution. Reject unknown names and unsupported argument types rather than guessing. Each accepted call must select exactly one PhysicalFunction; then call build_physical_expression with the inputs’ physical types and verify the returned metadata before publishing the bound result.

Run the complete logical-to-physical loop

The caller now starts with a logical schema, binds once, inspects the public contract, and evaluates the returned erased expression:

let call = LogicalCall::new(
    "+",
    [DataType::SmallInt, DataType::Integer],
);
let bound = bind_logical_call(call)?;

assert_eq!(bound.output_type(), &DataType::Integer);
assert_eq!(
    bound.physical_expression().input_types(),
    &[PhysicalType::Int16, PhysicalType::Int32],
);

let expression: Box<dyn Expression> = bound.into_physical_expression();
let output = expression.evaluate(&[left, right])?;

Binding performs logical overload and coercion selection once. Execution remains physical: the same whole-batch expression validates the actual columns and delegates to its specialized kernel. Planner trees and casts are outside this course boundary; List storage and async evaluation arrive in the final checkpoint.

Run the focused and cumulative checks:

cargo test -p type-exercise-starter-supplied-tests chapter_9 --locked
cargo test -p type-exercise-starter-supplied-tests --locked

The tests cover exact and widened numeric calls, mixed logical strings, Boolean and ternary binding, representative rejection paths, checked bound metadata, and evaluation through the returned erased expression. The result is a complete synchronous path from a logical call to an owned array.