mod rules;
use crate::core::{Schema, Value};
use crate::executor::context::ExecutionContext;
use crate::parser::ast::{self as ast};
use crate::storage::expression::Expression as StorageExpr;
pub use rules::*;
#[derive(Debug)]
pub enum PushdownResult {
Converted(Box<dyn StorageExpr>),
Partial(Box<dyn StorageExpr>),
NotApplicable,
CannotPush,
}
pub struct PushdownContext<'a> {
pub schema: &'a Schema,
pub exec_ctx: Option<&'a ExecutionContext>,
}
impl<'a> PushdownContext<'a> {
pub fn new(schema: &'a Schema, exec_ctx: Option<&'a ExecutionContext>) -> Self {
Self { schema, exec_ctx }
}
pub fn column_type(&self, name: &str) -> Option<crate::core::DataType> {
self.schema
.column_index_map()
.get(name)
.and_then(|&idx| self.schema.columns.get(idx).map(|c| c.data_type))
}
pub fn has_column(&self, name: &str) -> bool {
self.schema.has_column(name)
}
pub fn coerce_to_column_type(&self, column: &str, value: Value) -> Value {
if let Some(col_type) = self.column_type(column) {
value.into_coerce_to_type(col_type)
} else {
value
}
}
}
pub trait PushdownRule: Send + Sync {
fn name(&self) -> &'static str;
fn try_convert(&self, expr: &ast::Expression, ctx: &PushdownContext<'_>) -> PushdownResult;
}
pub struct PushdownRegistry {
rules: Vec<Box<dyn PushdownRule>>,
}
impl Default for PushdownRegistry {
fn default() -> Self {
Self::new()
}
}
impl PushdownRegistry {
pub fn new() -> Self {
let mut registry = Self { rules: vec![] };
registry.register(Box::new(LogicalAndRule));
registry.register(Box::new(LogicalOrRule));
registry.register(Box::new(LogicalNotRule));
registry.register(Box::new(LogicalXorRule));
registry.register(Box::new(BetweenRule));
registry.register(Box::new(InListRule));
registry.register(Box::new(LikeRule));
registry.register(Box::new(NullCheckRule));
registry.register(Box::new(BooleanCheckRule));
registry.register(Box::new(ComparisonRule));
registry.register(Box::new(FunctionRule));
registry.register(Box::new(BooleanLiteralRule));
registry
}
pub fn register(&mut self, rule: Box<dyn PushdownRule>) {
self.rules.push(rule);
}
pub fn try_pushdown(
&self,
expr: &ast::Expression,
schema: &Schema,
exec_ctx: Option<&ExecutionContext>,
) -> (Option<Box<dyn StorageExpr>>, bool) {
let ctx = PushdownContext::new(schema, exec_ctx);
self.try_pushdown_with_ctx(expr, &ctx)
}
pub(crate) fn try_pushdown_with_ctx(
&self,
expr: &ast::Expression,
ctx: &PushdownContext<'_>,
) -> (Option<Box<dyn StorageExpr>>, bool) {
for rule in &self.rules {
match rule.try_convert(expr, ctx) {
PushdownResult::Converted(storage_expr) => {
return (Some(storage_expr), false);
}
PushdownResult::Partial(storage_expr) => {
return (Some(storage_expr), true);
}
PushdownResult::CannotPush => {
return (None, true);
}
PushdownResult::NotApplicable => {
continue;
}
}
}
(None, true)
}
pub(crate) fn convert_expr(
&self,
expr: &ast::Expression,
ctx: &PushdownContext<'_>,
) -> Option<Box<dyn StorageExpr>> {
let (storage_expr, _) = self.try_pushdown_with_ctx(expr, ctx);
storage_expr
}
}
use std::sync::OnceLock;
static REGISTRY: OnceLock<PushdownRegistry> = OnceLock::new();
pub fn registry() -> &'static PushdownRegistry {
REGISTRY.get_or_init(PushdownRegistry::new)
}
pub fn try_pushdown(
expr: &ast::Expression,
schema: &Schema,
exec_ctx: Option<&ExecutionContext>,
) -> (Option<Box<dyn StorageExpr>>, bool) {
registry().try_pushdown(expr, schema, exec_ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{DataType, Row, SchemaBuilder};
use crate::parser::token::{Position, Token, TokenType};
fn test_schema() -> Schema {
SchemaBuilder::new("test")
.add_primary_key("id", DataType::Integer)
.add("name", DataType::Text)
.add("age", DataType::Integer)
.add_nullable("email", DataType::Text)
.add("active", DataType::Boolean)
.add("price", DataType::Float)
.build()
}
fn test_row() -> Row {
Row::from_values(vec![
Value::integer(1),
Value::text("Alice"),
Value::integer(30),
Value::text("alice@example.com"),
Value::Boolean(true),
Value::Float(99.99),
])
}
fn dummy_token() -> Token {
Token::new(TokenType::Error, "", Position::new(0, 1, 1))
}
fn make_ident(name: &str) -> ast::Expression {
ast::Expression::Identifier(ast::Identifier::new(dummy_token(), name.to_string()))
}
fn make_int(value: i64) -> ast::Expression {
ast::Expression::IntegerLiteral(ast::IntegerLiteral {
token: dummy_token(),
value,
})
}
fn make_str(value: &str) -> ast::Expression {
ast::Expression::StringLiteral(ast::StringLiteral {
token: dummy_token(),
value: value.into(),
type_hint: None,
})
}
fn make_infix(left: ast::Expression, op: &str, right: ast::Expression) -> ast::Expression {
ast::Expression::Infix(ast::InfixExpression::new(
dummy_token(),
Box::new(left),
op,
Box::new(right),
))
}
#[test]
fn test_simple_equality() {
let schema = test_schema();
let expr = make_infix(make_ident("id"), "=", make_int(1));
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap());
}
#[test]
fn test_and_expression() {
let schema = test_schema();
let left = make_infix(make_ident("id"), "=", make_int(1));
let right = make_infix(make_ident("age"), ">", make_int(20));
let expr = make_infix(left, "AND", right);
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap());
}
#[test]
fn test_function_pushable() {
let schema = test_schema();
let func = ast::Expression::FunctionCall(Box::new(ast::FunctionCall {
token: dummy_token(),
function: "LENGTH".into(),
arguments: vec![make_ident("name")],
is_distinct: false,
order_by: vec![],
filter: None,
}));
let expr = make_infix(func, ">", make_int(5));
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
}
#[test]
fn test_full_pushdown_with_function() {
let schema = test_schema();
let pushable = make_infix(make_ident("id"), "=", make_int(1));
let func = ast::Expression::FunctionCall(Box::new(ast::FunctionCall {
token: dummy_token(),
function: "LENGTH".into(),
arguments: vec![make_ident("name")],
is_distinct: false,
order_by: vec![],
filter: None,
}));
let also_pushable = make_infix(func, ">=", make_int(5)); let expr = make_infix(pushable, "AND", also_pushable);
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap());
}
#[test]
fn test_between() {
let schema = test_schema();
let expr = ast::Expression::Between(ast::BetweenExpression {
token: dummy_token(),
expr: Box::new(make_ident("age")),
lower: Box::new(make_int(25)),
upper: Box::new(make_int(35)),
not: false,
});
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap()); }
#[test]
fn test_in_list() {
let schema = test_schema();
let expr = ast::Expression::In(ast::InExpression {
token: dummy_token(),
left: Box::new(make_ident("id")),
right: Box::new(ast::Expression::ExpressionList(Box::new(
ast::ExpressionList {
token: dummy_token(),
expressions: vec![make_int(1), make_int(2), make_int(3)],
},
))),
not: false,
});
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap()); }
#[test]
fn test_like() {
let schema = test_schema();
let expr = ast::Expression::Like(ast::LikeExpression {
token: dummy_token(),
left: Box::new(make_ident("name")),
operator: "LIKE".into(),
pattern: Box::new(make_str("Ali%")),
escape: None,
});
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap()); }
#[test]
fn test_is_null() {
let schema = test_schema();
let expr = make_infix(
make_ident("email"),
"IS",
ast::Expression::NullLiteral(ast::NullLiteral {
token: dummy_token(),
}),
);
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(!expr.evaluate(&test_row()).unwrap());
}
#[test]
fn test_or_fully_pushable() {
let schema = test_schema();
let left = make_infix(make_ident("id"), "=", make_int(1));
let right = make_infix(make_ident("id"), "=", make_int(2));
let expr = make_infix(left, "OR", right);
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
}
#[test]
fn test_or_with_function_pushable() {
let schema = test_schema();
let left = make_infix(make_ident("id"), "=", make_int(1));
let func = ast::Expression::FunctionCall(Box::new(ast::FunctionCall {
token: dummy_token(),
function: "LENGTH".into(),
arguments: vec![make_ident("name")],
is_distinct: false,
order_by: vec![],
filter: None,
}));
let right = make_infix(func, ">", make_int(5));
let expr = make_infix(left, "OR", right);
let (storage_expr, needs_mem) = try_pushdown(&expr, &schema, None);
assert!(storage_expr.is_some());
assert!(!needs_mem);
let mut expr = storage_expr.unwrap();
expr.prepare_for_schema(&schema);
assert!(expr.evaluate(&test_row()).unwrap());
}
}