Skip to content

Commit b97717a

Browse files
committed
Merge phase22a-window-functions: window frontend + oracle
2 parents 788fcd7 + a710266 commit b97717a

18 files changed

Lines changed: 1437 additions & 28 deletions

‎ARCHITECTURE.md‎

Lines changed: 24 additions & 6 deletions
Large diffs are not rendered by default.

‎ROADMAP.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
19. Boolean-mask filter kernels. Delivered in Phase 19: vectorized Filter now evaluates WHERE/HAVING predicate trees as paired `is_true`/`is_known` byte masks using SQL Kleene 3VL algebra, with comparison leaves scanning pre-resolved int64 or string values column-at-a-time and `IS [NOT] NULL` leaves reading validity directly. Scan-adjacent identity selections use a dense full-batch domain, while filtered/non-identity inputs use dense selection-position masks that map back to the immutable input selection. Join residual predicates deliberately remain on the existing row-pair path. Release benchmark evidence in `docs/benchmarks.md` shows the three scan/filter vectorized medians improving with byte-identical correctness checks and no material non-target regression.
2222
20. LEFT/RIGHT OUTER JOIN arc. Delivered across Phase 20a/20b: the parser accepts `LEFT [OUTER] JOIN` and `RIGHT [OUTER] JOIN`; binding normalizes RIGHT to LEFT while stable identities preserve SQL-visible projection order; both interpreted and vectorized engines implement deterministic left-row-major NULL-extension, NULL-key non-matching, and residual-aware matchedness for hash and non-equi joins. `LeftJoinToInnerRule` additively simplifies a filtered LEFT join only under a proof-bearing conservative 3VL null-rejection test, unlocking the existing INNER commute/associate/pushdown exploration without weakening their guards. The deterministic fuzzer now mixes INNER/LEFT/RIGHT chains with actual NULL-bearing key data and verifies standalone rewrites, every memo alternative, and `extract_best` through both engines. The Phase 20a LEFT cardinality rule remains sufficient; no 20b cost-model change was required. The outer-join lowering guard and its verifier path are gone.
2323
21. Complete subquery arc. Delivered across Phase 21a/21b/21c: scalar-comparison, IN/NOT IN, and EXISTS/NOT EXISTS use immutable bound subplans with explicit lexical-depth correlation metadata. Empty correlation retains the eager once-per-query 3VL/cardinality model; correlated forms use the per-outer-row interpreted specification with nested/grandparent frames and row-ordered scalar errors. Explicit left SEMI/ANTI algebra has left-only output identity, deterministic interpreted/vectorized hash/NLJ execution, memo identity/printers/EXPLAIN/costing, and conservative guards. Uncorrelated and immediate equality-correlated top-level EXISTS, NOT EXISTS, and IN decorrelate through proof-bearing memo rules. Residual correlated forms remain oracle-defined and fail once at physical lowering; uncorrelated residuals still run vectorized recursively. Exact EXPLAIN goldens, eager/per-row error equivalence, and deterministic uncorrelated/correlated fuzz lanes close the story.
24-
22. Null-aware anti join (future). Extend algebra and execution before considering NOT IN decorrelation; ordinary Anti remains intentionally insufficient for a NULL-bearing right set, where unmatched NOT IN is UNKNOWN rather than TRUE.
24+
22. Window functions arc. Phase 22a is delivered: whole-SELECT-item `ROW_NUMBER`, `RANK`, and `DENSE_RANK` plus whole-partition `SUM`, `COUNT`, `MIN`, and `MAX` parse and bind into one post-HAVING Window node; the interpreted oracle defines first-appearance partitioning, input-order preservation, stable ranking ties, NULL partition/peer equality, and replicated NULL-aware aggregate results; Window participates in memo identity, EXPLAIN, extraction, and conservative costing while remaining a filter-pushdown barrier, with ROW_NUMBER and checked SUM additionally pinning order-changing child transforms because stable ties and intermediate overflow expose input order; physical lowering fails with one stable unsupported error. Phase 22b will add the real vectorized Window operator and deterministic fuzzer generation. Running aggregate windows and explicit `ROWS`/`RANGE` frames remain future work after 22b.
25+
23. Null-aware anti join (future). Extend algebra and execution before considering NOT IN decorrelation; ordinary Anti remains intentionally insufficient for a NULL-bearing right set, where unmatched NOT IN is UNKNOWN rather than TRUE.
2526

2627
## Phase 1 first tasks
2728

@@ -44,3 +45,4 @@ Do not optimize before correctness. Every phase should end with an executable de
4445
- Broaden fuzz coverage when the verifier can prove sortedness for ORDER BY keys that are not emitted in the final output.
4546
- Add value-width-aware string costing and string-specific performance work after collecting more benchmark evidence.
4647
- Investigate aggregation and sort kernels with benchmark evidence before changing their implementations.
48+
- Add running window frames (`ORDER BY` for aggregate windows and explicit `ROWS`/`RANGE BETWEEN`) only after frame bounds are represented explicitly in the algebra and both engines can share differential semantics.

‎include/optimizer/memo.hpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace optimizer {
1414

1515
using GroupId = std::uint64_t;
1616

17-
enum class MemoExpressionKind { Scan, Join, Filter, Project, Aggregate, Distinct, Sort, Limit, GroupRef };
17+
enum class MemoExpressionKind { Scan, Join, Filter, Project, Aggregate, Window, Distinct, Sort, Limit, GroupRef };
1818

1919
struct MemoExpression {
2020
MemoExpressionKind kind{MemoExpressionKind::Scan};
@@ -26,6 +26,7 @@ struct MemoExpression {
2626
std::vector<plan::Projection> projections;
2727
std::vector<plan::BoundColumnRef> group_keys;
2828
std::vector<plan::AggregateExpression> aggregate_expressions;
29+
std::vector<plan::WindowExpression> window_expressions;
2930
std::vector<plan::SortKey> sort_keys;
3031
std::vector<plan::BoundPredicate> predicates;
3132
plan::JoinKind join_kind{plan::JoinKind::Inner};

‎include/plan/logical_plan.hpp‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace plan {
1616

1717
struct LogicalPlan;
1818

19-
enum class LogicalKind { Scan, Join, Filter, Project, Aggregate, Distinct, Sort, Limit, Explain };
19+
enum class LogicalKind { Scan, Join, Filter, Project, Aggregate, Window, Distinct, Sort, Limit, Explain };
2020
enum class OrderPermission { Deterministic, Arbitrary };
2121
enum class JoinKind { Inner, Left, Semi, Anti };
2222

@@ -149,6 +149,17 @@ struct AggregateExpression {
149149
catalog::ColumnType type{catalog::ColumnType::Int64};
150150
};
151151

152+
struct WindowExpression {
153+
std::string output_name;
154+
sql::WindowFunction function{sql::WindowFunction::RowNumber};
155+
bool count_star{false};
156+
std::optional<BoundColumnRef> argument;
157+
std::vector<BoundColumnRef> partition_keys;
158+
std::vector<SortKey> order_keys;
159+
std::size_t position{0};
160+
catalog::ColumnType type{catalog::ColumnType::Int64};
161+
};
162+
152163
struct LogicalPlan {
153164
LogicalKind kind{LogicalKind::Scan};
154165
OrderPermission order_permission{OrderPermission::Deterministic};
@@ -159,6 +170,7 @@ struct LogicalPlan {
159170
std::vector<Projection> projections;
160171
std::vector<BoundColumnRef> group_keys;
161172
std::vector<AggregateExpression> aggregate_expressions;
173+
std::vector<WindowExpression> window_expressions;
162174
std::vector<SortKey> sort_keys;
163175
std::vector<BoundPredicate> predicates;
164176
// Set only on a bound subplan root. Empty is the structural Phase 21a
@@ -228,6 +240,14 @@ struct LogicalPlan {
228240
return p;
229241
}
230242

243+
static LogicalPlan window(std::vector<WindowExpression> window_expressions, LogicalPlan child) {
244+
LogicalPlan p;
245+
p.kind = LogicalKind::Window;
246+
p.window_expressions = std::move(window_expressions);
247+
p.input = std::make_shared<LogicalPlan>(std::move(child));
248+
return p;
249+
}
250+
231251
static LogicalPlan distinct(LogicalPlan child) {
232252
LogicalPlan p;
233253
p.kind = LogicalKind::Distinct;

‎include/sql/ast.hpp‎

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,31 @@ struct AggregateCall {
4949
bool nested_aggregate{false};
5050
AggregateFunction nested_function{AggregateFunction::Count};
5151
std::size_t nested_position{0};
52+
std::shared_ptr<AggregateCall> nested_call;
5253
};
5354

54-
using SelectExpr = std::variant<ScalarExpr, AggregateCall>;
55+
enum class SortDirection { Asc, Desc };
56+
57+
enum class WindowFunction { RowNumber, Rank, DenseRank, Count, Sum, Min, Max };
58+
59+
using WindowInputExpr = std::variant<ColumnRef, AggregateCall>;
60+
61+
struct WindowOrderKey {
62+
WindowInputExpr expression;
63+
SortDirection direction{SortDirection::Asc};
64+
};
65+
66+
struct WindowCall {
67+
WindowFunction function{WindowFunction::RowNumber};
68+
std::size_t position{0};
69+
std::size_t over_position{0};
70+
bool count_star{false};
71+
std::optional<WindowInputExpr> argument;
72+
std::vector<WindowInputExpr> partition_by;
73+
std::vector<WindowOrderKey> order_by;
74+
};
75+
76+
using SelectExpr = std::variant<ScalarExpr, AggregateCall, WindowCall>;
5577
using HavingExpr = std::variant<ColumnRef, IntLiteral, StringLiteral, NullLiteral, ScalarSubquery, AggregateCall>;
5678
using OrderByExpr = std::variant<ColumnRef, AggregateCall>;
5779

@@ -201,8 +223,6 @@ struct HavingClause {
201223
std::vector<HavingPredicateExpr> conjuncts;
202224
};
203225

204-
enum class SortDirection { Asc, Desc };
205-
206226
struct OrderByKey {
207227
OrderByExpr expression;
208228
SortDirection direction{SortDirection::Asc};
@@ -264,7 +284,10 @@ inline std::size_t expression_position(const SelectExpr& expression) {
264284
if (const auto* scalar = std::get_if<ScalarExpr>(&expression)) {
265285
return expression_position(*scalar);
266286
}
267-
return std::get<AggregateCall>(expression).position;
287+
if (const auto* aggregate = std::get_if<AggregateCall>(&expression)) {
288+
return aggregate->position;
289+
}
290+
return std::get<WindowCall>(expression).position;
268291
}
269292

270293
inline std::size_t expression_position(const HavingExpr& expression) {
@@ -307,6 +330,26 @@ inline std::string aggregate_function_name(AggregateFunction function) {
307330
return "<unknown aggregate>";
308331
}
309332

333+
inline std::string window_function_name(WindowFunction function) {
334+
switch (function) {
335+
case WindowFunction::RowNumber:
336+
return "ROW_NUMBER";
337+
case WindowFunction::Rank:
338+
return "RANK";
339+
case WindowFunction::DenseRank:
340+
return "DENSE_RANK";
341+
case WindowFunction::Count:
342+
return "COUNT";
343+
case WindowFunction::Sum:
344+
return "SUM";
345+
case WindowFunction::Min:
346+
return "MIN";
347+
case WindowFunction::Max:
348+
return "MAX";
349+
}
350+
return "<unknown window function>";
351+
}
352+
310353
inline std::string quote_string_literal(const std::string& value) {
311354
std::string quoted = "'";
312355
for (const auto ch : value) {
@@ -351,18 +394,64 @@ inline std::string output_name(const AggregateCall& aggregate) {
351394
name += "*";
352395
} else if (aggregate.argument.has_value()) {
353396
name += output_name(*aggregate.argument);
397+
} else if (aggregate.nested_call != nullptr) {
398+
name += output_name(*aggregate.nested_call);
354399
} else if (aggregate.nested_aggregate) {
355400
name += aggregate_function_name(aggregate.nested_function) + "(...)";
356401
}
357402
name += ")";
358403
return name;
359404
}
360405

406+
inline std::string window_input_output_name(const WindowInputExpr& expression) {
407+
if (const auto* column = std::get_if<ColumnRef>(&expression)) {
408+
return output_name(*column);
409+
}
410+
return output_name(std::get<AggregateCall>(expression));
411+
}
412+
413+
inline std::string output_name(const WindowCall& window) {
414+
auto name = window_function_name(window.function) + "(";
415+
if (window.count_star) {
416+
name += "*";
417+
} else if (window.argument.has_value()) {
418+
name += window_input_output_name(*window.argument);
419+
}
420+
name += ") OVER (";
421+
if (!window.partition_by.empty()) {
422+
name += "PARTITION BY ";
423+
for (std::size_t i = 0; i < window.partition_by.size(); ++i) {
424+
if (i != 0) {
425+
name += ", ";
426+
}
427+
name += window_input_output_name(window.partition_by[i]);
428+
}
429+
}
430+
if (!window.order_by.empty()) {
431+
if (!window.partition_by.empty()) {
432+
name += " ";
433+
}
434+
name += "ORDER BY ";
435+
for (std::size_t i = 0; i < window.order_by.size(); ++i) {
436+
if (i != 0) {
437+
name += ", ";
438+
}
439+
name += window_input_output_name(window.order_by[i].expression);
440+
name += window.order_by[i].direction == SortDirection::Asc ? " ASC" : " DESC";
441+
}
442+
}
443+
name += ")";
444+
return name;
445+
}
446+
361447
inline std::string output_name(const SelectExpr& expression) {
362448
if (const auto* scalar = std::get_if<ScalarExpr>(&expression)) {
363449
return output_name(*scalar);
364450
}
365-
return output_name(std::get<AggregateCall>(expression));
451+
if (const auto* aggregate = std::get_if<AggregateCall>(&expression)) {
452+
return output_name(*aggregate);
453+
}
454+
return output_name(std::get<WindowCall>(expression));
366455
}
367456

368457
inline std::string output_name(const HavingExpr& expression) {

0 commit comments

Comments
 (0)