Skip to content

Commit 9f9e2b3

Browse files
committed
feat: add rule-based optimizer — predicate pushdown, constant folding, projection pruning, limit pushdown
1 parent 10b8612 commit 9f9e2b3

7 files changed

Lines changed: 1227 additions & 1 deletion

File tree

Makefile.new

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ TEST_SRCS = $(TEST_DIR)/test_main.cpp \
5555
$(TEST_DIR)/test_row.cpp \
5656
$(TEST_DIR)/test_plan_builder.cpp \
5757
$(TEST_DIR)/test_operators.cpp \
58-
$(TEST_DIR)/test_plan_executor.cpp
58+
$(TEST_DIR)/test_plan_executor.cpp \
59+
$(TEST_DIR)/test_optimizer.cpp
5960
TEST_OBJS = $(TEST_SRCS:.cpp=.o)
6061
TEST_TARGET = $(PROJECT_ROOT)/run_tests
6162

include/sql_engine/optimizer.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// optimizer.h — Rule-based query optimizer
2+
//
3+
// Optimizer<D> applies four rewrite rules in sequence to transform a logical
4+
// plan into a more efficient logical plan:
5+
// 1. Predicate pushdown — push filters below joins
6+
// 2. Projection pruning — annotate needed columns (no-op for now)
7+
// 3. Constant folding — evaluate constant sub-expressions at plan time
8+
// 4. Limit pushdown — push limits past filters toward scans
9+
10+
#ifndef SQL_ENGINE_OPTIMIZER_H
11+
#define SQL_ENGINE_OPTIMIZER_H
12+
13+
#include "sql_engine/plan_node.h"
14+
#include "sql_engine/catalog.h"
15+
#include "sql_engine/function_registry.h"
16+
#include "sql_engine/rules/predicate_pushdown.h"
17+
#include "sql_engine/rules/projection_pruning.h"
18+
#include "sql_engine/rules/constant_folding.h"
19+
#include "sql_engine/rules/limit_pushdown.h"
20+
#include "sql_parser/arena.h"
21+
#include "sql_parser/common.h"
22+
23+
namespace sql_engine {
24+
25+
template <sql_parser::Dialect D>
26+
class Optimizer {
27+
public:
28+
Optimizer(const Catalog& catalog, FunctionRegistry<D>& functions)
29+
: catalog_(catalog), functions_(functions) {}
30+
31+
PlanNode* optimize(PlanNode* plan, sql_parser::Arena& arena) {
32+
if (!plan) return nullptr;
33+
34+
plan = rules::predicate_pushdown(plan, catalog_, arena);
35+
plan = rules::projection_pruning(plan, catalog_, arena);
36+
plan = rules::constant_folding<D>(plan, catalog_, functions_, arena);
37+
plan = rules::limit_pushdown(plan, catalog_, arena);
38+
39+
return plan;
40+
}
41+
42+
private:
43+
const Catalog& catalog_;
44+
FunctionRegistry<D>& functions_;
45+
};
46+
47+
} // namespace sql_engine
48+
49+
#endif // SQL_ENGINE_OPTIMIZER_H
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// constant_folding.h — Evaluate sub-expressions that have no column references
2+
// at plan time, replacing them with literal AST nodes.
3+
//
4+
// Walks all expression ASTs in Filter, Project, Sort conditions. For each
5+
// sub-expression with zero column references, evaluates it using
6+
// evaluate_expression with a null resolver and replaces the node in-place.
7+
8+
#ifndef SQL_ENGINE_RULES_CONSTANT_FOLDING_H
9+
#define SQL_ENGINE_RULES_CONSTANT_FOLDING_H
10+
11+
#include "sql_engine/plan_node.h"
12+
#include "sql_engine/catalog.h"
13+
#include "sql_engine/expression_eval.h"
14+
#include "sql_engine/function_registry.h"
15+
#include "sql_parser/ast.h"
16+
#include "sql_parser/arena.h"
17+
#include "sql_parser/common.h"
18+
#include <cstdio>
19+
#include <cstring>
20+
21+
namespace sql_engine {
22+
namespace rules {
23+
24+
namespace detail_cf {
25+
26+
// Check if an expression has any column references
27+
inline bool has_column_ref(const sql_parser::AstNode* expr) {
28+
if (!expr) return false;
29+
switch (expr->type) {
30+
case sql_parser::NodeType::NODE_COLUMN_REF:
31+
case sql_parser::NodeType::NODE_QUALIFIED_NAME:
32+
return true;
33+
case sql_parser::NodeType::NODE_IDENTIFIER:
34+
// Identifiers in expression context are column references
35+
return true;
36+
default:
37+
break;
38+
}
39+
for (const sql_parser::AstNode* c = expr->first_child; c; c = c->next_sibling) {
40+
if (has_column_ref(c)) return true;
41+
}
42+
return false;
43+
}
44+
45+
// Try to fold a constant sub-expression. Returns true if folded.
46+
// Modifies the node in-place (arena-allocated, safe to mutate).
47+
template <sql_parser::Dialect D>
48+
inline bool try_fold(sql_parser::AstNode* expr,
49+
FunctionRegistry<D>& functions,
50+
sql_parser::Arena& arena) {
51+
if (!expr) return false;
52+
53+
// Don't fold leaf literals — already constant
54+
switch (expr->type) {
55+
case sql_parser::NodeType::NODE_LITERAL_INT:
56+
case sql_parser::NodeType::NODE_LITERAL_FLOAT:
57+
case sql_parser::NodeType::NODE_LITERAL_STRING:
58+
case sql_parser::NodeType::NODE_LITERAL_NULL:
59+
return false;
60+
default:
61+
break;
62+
}
63+
64+
// If this expression has no column refs, try to evaluate it
65+
if (!has_column_ref(expr)) {
66+
// Null resolver — any column reference attempt returns null
67+
auto null_resolve = [](sql_parser::StringRef) -> Value {
68+
return value_null();
69+
};
70+
71+
Value result = evaluate_expression<D>(expr, null_resolve, functions, arena);
72+
73+
if (result.is_null()) {
74+
expr->type = sql_parser::NodeType::NODE_LITERAL_NULL;
75+
expr->first_child = nullptr;
76+
expr->value_ptr = nullptr;
77+
expr->value_len = 0;
78+
return true;
79+
}
80+
81+
// Replace node with appropriate literal type
82+
switch (result.tag) {
83+
case Value::TAG_INT64: {
84+
// Format integer as string in arena
85+
char tmp[32];
86+
int n = std::snprintf(tmp, sizeof(tmp), "%lld",
87+
static_cast<long long>(result.int_val));
88+
if (n > 0) {
89+
char* buf = static_cast<char*>(arena.allocate(static_cast<uint32_t>(n)));
90+
std::memcpy(buf, tmp, static_cast<size_t>(n));
91+
expr->type = sql_parser::NodeType::NODE_LITERAL_INT;
92+
expr->value_ptr = buf;
93+
expr->value_len = static_cast<uint32_t>(n);
94+
expr->first_child = nullptr;
95+
return true;
96+
}
97+
break;
98+
}
99+
case Value::TAG_DOUBLE: {
100+
char tmp[64];
101+
int n = std::snprintf(tmp, sizeof(tmp), "%g", result.double_val);
102+
if (n > 0) {
103+
char* buf = static_cast<char*>(arena.allocate(static_cast<uint32_t>(n)));
104+
std::memcpy(buf, tmp, static_cast<size_t>(n));
105+
expr->type = sql_parser::NodeType::NODE_LITERAL_FLOAT;
106+
expr->value_ptr = buf;
107+
expr->value_len = static_cast<uint32_t>(n);
108+
expr->first_child = nullptr;
109+
return true;
110+
}
111+
break;
112+
}
113+
case Value::TAG_STRING: {
114+
expr->type = sql_parser::NodeType::NODE_LITERAL_STRING;
115+
expr->value_ptr = result.str_val.ptr;
116+
expr->value_len = result.str_val.len;
117+
expr->first_child = nullptr;
118+
return true;
119+
}
120+
case Value::TAG_BOOL: {
121+
const char* s = result.bool_val ? "TRUE" : "FALSE";
122+
uint32_t len = result.bool_val ? 4 : 5;
123+
char* buf = static_cast<char*>(arena.allocate(len));
124+
std::memcpy(buf, s, len);
125+
expr->type = sql_parser::NodeType::NODE_LITERAL_INT;
126+
expr->value_ptr = buf;
127+
expr->value_len = len;
128+
expr->first_child = nullptr;
129+
return true;
130+
}
131+
default:
132+
break;
133+
}
134+
return false;
135+
}
136+
137+
// Has column refs — try to fold children (partial folding)
138+
bool any_folded = false;
139+
for (sql_parser::AstNode* c = expr->first_child; c; c = c->next_sibling) {
140+
if (try_fold<D>(c, functions, arena)) {
141+
any_folded = true;
142+
}
143+
}
144+
return any_folded;
145+
}
146+
147+
// Fold all expressions in a plan node
148+
template <sql_parser::Dialect D>
149+
inline void fold_plan_exprs(PlanNode* node,
150+
FunctionRegistry<D>& functions,
151+
sql_parser::Arena& arena) {
152+
if (!node) return;
153+
154+
switch (node->type) {
155+
case PlanNodeType::FILTER:
156+
if (node->filter.expr) {
157+
try_fold<D>(const_cast<sql_parser::AstNode*>(node->filter.expr),
158+
functions, arena);
159+
}
160+
break;
161+
case PlanNodeType::PROJECT:
162+
for (uint16_t i = 0; i < node->project.count; ++i) {
163+
if (node->project.exprs[i]) {
164+
try_fold<D>(const_cast<sql_parser::AstNode*>(node->project.exprs[i]),
165+
functions, arena);
166+
}
167+
}
168+
break;
169+
case PlanNodeType::SORT:
170+
for (uint16_t i = 0; i < node->sort.count; ++i) {
171+
if (node->sort.keys[i]) {
172+
try_fold<D>(const_cast<sql_parser::AstNode*>(node->sort.keys[i]),
173+
functions, arena);
174+
}
175+
}
176+
break;
177+
case PlanNodeType::JOIN:
178+
if (node->join.condition) {
179+
try_fold<D>(const_cast<sql_parser::AstNode*>(node->join.condition),
180+
functions, arena);
181+
}
182+
break;
183+
case PlanNodeType::AGGREGATE:
184+
for (uint16_t i = 0; i < node->aggregate.group_count; ++i) {
185+
if (node->aggregate.group_by[i]) {
186+
try_fold<D>(const_cast<sql_parser::AstNode*>(node->aggregate.group_by[i]),
187+
functions, arena);
188+
}
189+
}
190+
for (uint16_t i = 0; i < node->aggregate.agg_count; ++i) {
191+
if (node->aggregate.agg_exprs[i]) {
192+
try_fold<D>(const_cast<sql_parser::AstNode*>(node->aggregate.agg_exprs[i]),
193+
functions, arena);
194+
}
195+
}
196+
break;
197+
default:
198+
break;
199+
}
200+
}
201+
202+
} // namespace detail_cf
203+
204+
template <sql_parser::Dialect D>
205+
inline PlanNode* constant_folding(PlanNode* node, const Catalog& catalog,
206+
FunctionRegistry<D>& functions,
207+
sql_parser::Arena& arena) {
208+
if (!node) return nullptr;
209+
210+
// Fold expressions in this node
211+
detail_cf::fold_plan_exprs<D>(node, functions, arena);
212+
213+
// Recurse into children
214+
node->left = constant_folding<D>(node->left, catalog, functions, arena);
215+
node->right = constant_folding<D>(node->right, catalog, functions, arena);
216+
217+
return node;
218+
}
219+
220+
} // namespace rules
221+
} // namespace sql_engine
222+
223+
#endif // SQL_ENGINE_RULES_CONSTANT_FOLDING_H
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// limit_pushdown.h — Push Limit nodes past Filter nodes toward Scan nodes.
2+
//
3+
// When we see Limit -> Filter -> child (with no Sort/Aggregate/Distinct/Join
4+
// between), insert a new Limit node between Filter and child with the same
5+
// count. The inner Limit is a hint for early termination.
6+
7+
#ifndef SQL_ENGINE_RULES_LIMIT_PUSHDOWN_H
8+
#define SQL_ENGINE_RULES_LIMIT_PUSHDOWN_H
9+
10+
#include "sql_engine/plan_node.h"
11+
#include "sql_engine/catalog.h"
12+
#include "sql_parser/arena.h"
13+
14+
namespace sql_engine {
15+
namespace rules {
16+
17+
inline PlanNode* limit_pushdown(PlanNode* node, const Catalog& catalog,
18+
sql_parser::Arena& arena) {
19+
if (!node) return nullptr;
20+
21+
// Recurse into children first
22+
node->left = limit_pushdown(node->left, catalog, arena);
23+
node->right = limit_pushdown(node->right, catalog, arena);
24+
25+
// Pattern: Limit -> Filter -> child
26+
if (node->type != PlanNodeType::LIMIT) return node;
27+
if (!node->left || node->left->type != PlanNodeType::FILTER) return node;
28+
29+
PlanNode* filter = node->left;
30+
PlanNode* filter_child = filter->left;
31+
32+
// Don't push if the filter's child is Sort, Aggregate, Distinct, or Join
33+
if (filter_child) {
34+
switch (filter_child->type) {
35+
case PlanNodeType::SORT:
36+
case PlanNodeType::AGGREGATE:
37+
case PlanNodeType::DISTINCT:
38+
case PlanNodeType::JOIN:
39+
return node; // blocked
40+
default:
41+
break;
42+
}
43+
}
44+
45+
// Insert a new Limit between Filter and its child
46+
PlanNode* inner_limit = make_plan_node(arena, PlanNodeType::LIMIT);
47+
inner_limit->limit.count = node->limit.count;
48+
inner_limit->limit.offset = 0; // inner limit doesn't need offset
49+
inner_limit->left = filter_child;
50+
51+
filter->left = inner_limit;
52+
53+
return node;
54+
}
55+
56+
} // namespace rules
57+
} // namespace sql_engine
58+
59+
#endif // SQL_ENGINE_RULES_LIMIT_PUSHDOWN_H

0 commit comments

Comments
 (0)