Skip to content

Commit 192e449

Browse files
committed
Merge phase23-running-frames: running frames + checked-SUM order-pin soundness fix
2 parents 1e865cc + 4987c86 commit 192e449

16 files changed

Lines changed: 896 additions & 106 deletions

‎ARCHITECTURE.md‎

Lines changed: 16 additions & 10 deletions
Large diffs are not rendered by default.

‎ROADMAP.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
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. Window functions arc. Delivered across Phase 22a/22b: 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; both interpreted and vectorized execution implement first-appearance partitioning, input-order preservation, stable ranking ties, NULL partition/peer equality, checked/NULL-skipping partition aggregates, and replicated results; Window participates in physical lowering, memo identity, EXPLAIN, extraction, conservative costing, golden coverage, deterministic fuzzing, and a Release benchmark workload 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. Running aggregate windows and explicit `ROWS`/`RANGE` frames remain future work.
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.
24+
22. Window functions arc. Delivered across Phase 22a/22b: 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; both interpreted and vectorized execution implement first-appearance partitioning, input-order preservation, stable ranking ties, NULL partition/peer equality, checked/NULL-skipping partition aggregates, and replicated results; Window participates in physical lowering, memo identity, EXPLAIN, extraction, conservative costing, golden coverage, deterministic fuzzing, and a Release benchmark workload while remaining a filter-pushdown barrier, with ROW_NUMBER and whole-partition checked SUM pinning order-changing child transforms because stable ties and intermediate overflow expose input order.
25+
23. Running window frames. Delivered in Phase 23: aggregate OVER ORDER BY defaults to `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`, and the same explicit RANGE spelling plus `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` bind as distinct frame modes. RANGE publishes one result through each complete peer group and canonically orders checked SUM arguments within peers, making output and overflow category independent of incidental tie order. ROWS publishes each stable input-order prefix and pins order-changing child transforms; a checked Aggregate SUM below any Window pins independently. The oracle, vectorized operator, memo alternatives, deterministic fuzzer, goldens, and standalone benchmark cover the new paths while whole-partition behavior remains unchanged. Every other frame bound or modifier fails as a positioned unsupported frame.
26+
24. 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.
2627

2728
## Phase 1 first tasks
2829

@@ -45,4 +46,4 @@ Do not optimize before correctness. Every phase should end with an executable de
4546
- Broaden fuzz coverage when the verifier can prove sortedness for ORDER BY keys that are not emitted in the final output.
4647
- Add value-width-aware string costing and string-specific performance work after collecting more benchmark evidence.
4748
- 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.
49+
- Add bounded, following, `GROUPS`, and exclusion window frames only after their endpoints and exclusion rules are represented explicitly in the algebra and their order/error observability is proved across memo alternatives.

‎benchmarks/sql_bench.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,11 @@ std::vector<Workload> make_workloads(const execution::Catalog& catalog) {
366366
"ROW_NUMBER() OVER (PARTITION BY bucket ORDER BY sort_key) AS rn, "
367367
"SUM(value) OVER (PARTITION BY bucket) AS bucket_sum "
368368
"FROM fact");
369+
add("window_running_sum",
370+
"fact=200000,partitions=100",
371+
"SELECT bucket, sort_key, value, "
372+
"SUM(value) OVER (PARTITION BY bucket ORDER BY sort_key) AS running_sum "
373+
"FROM fact");
369374
add("sort",
370375
"fact=200000",
371376
"SELECT a, value FROM fact ORDER BY sort_key, a LIMIT 1000");

‎docs/benchmarks.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,15 @@ phase.
248248
| workload | rows | correctness | interpreted min ms | interpreted median ms | vectorized min ms | vectorized median ms | median speedup |
249249
|---|---:|---|---:|---:|---:|---:|---:|
250250
| window_row_number_sum | fact=200000,partitions=100 | match rows=200000 checksum=0x1f5b8dc9ce10a38c | 725.385 | 732.916 | 108.253 | 109.029 | 6.722x |
251+
252+
## Phase 23 Running Window Frame (2026-07-13)
253+
254+
Same optimized build, five-repetition min/median, deterministic-data, and
255+
checksum-before-timing methodology as above. This workload measures the
256+
default cumulative `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` frame
257+
for `SUM(value) OVER (PARTITION BY bucket ORDER BY sort_key)` across the
258+
200,000-row `fact` table and its 100 partitions.
259+
260+
| workload | rows | correctness | interpreted min ms | interpreted median ms | vectorized min ms | vectorized median ms | median speedup |
261+
|---|---:|---|---:|---:|---:|---:|---:|
262+
| window_running_sum | fact=200000,partitions=100 | match rows=200000 checksum=0xefdfa729da68f560 | 704.578 | 711.366 | 102.685 | 105.883 | 6.718x |

‎include/plan/logical_plan.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ struct WindowExpression {
156156
std::optional<BoundColumnRef> argument;
157157
std::vector<BoundColumnRef> partition_keys;
158158
std::vector<SortKey> order_keys;
159+
sql::WindowFrame frame{sql::WindowFrame::WholePartition};
159160
std::size_t position{0};
160161
catalog::ColumnType type{catalog::ColumnType::Int64};
161162
};

‎include/sql/ast.hpp‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ struct AggregateCall {
5555
enum class SortDirection { Asc, Desc };
5656

5757
enum class WindowFunction { RowNumber, Rank, DenseRank, Count, Sum, Min, Max };
58+
enum class WindowFrame { WholePartition, RangeCumulative, RowsCumulative };
5859

5960
using WindowInputExpr = std::variant<ColumnRef, AggregateCall>;
6061

@@ -71,6 +72,8 @@ struct WindowCall {
7172
std::optional<WindowInputExpr> argument;
7273
std::vector<WindowInputExpr> partition_by;
7374
std::vector<WindowOrderKey> order_by;
75+
std::optional<WindowFrame> explicit_frame;
76+
std::size_t frame_position{0};
7477
};
7578

7679
using SelectExpr = std::variant<ScalarExpr, AggregateCall, WindowCall>;
@@ -350,6 +353,18 @@ inline std::string window_function_name(WindowFunction function) {
350353
return "<unknown window function>";
351354
}
352355

356+
inline std::string window_frame_name(WindowFrame frame) {
357+
switch (frame) {
358+
case WindowFrame::WholePartition:
359+
return "WHOLE PARTITION";
360+
case WindowFrame::RangeCumulative:
361+
return "RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW";
362+
case WindowFrame::RowsCumulative:
363+
return "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW";
364+
}
365+
return "<unknown window frame>";
366+
}
367+
353368
inline std::string quote_string_literal(const std::string& value) {
354369
std::string quoted = "'";
355370
for (const auto ch : value) {
@@ -418,6 +433,7 @@ inline std::string output_name(const WindowCall& window) {
418433
name += window_input_output_name(*window.argument);
419434
}
420435
name += ") OVER (";
436+
bool wrote_clause = false;
421437
if (!window.partition_by.empty()) {
422438
name += "PARTITION BY ";
423439
for (std::size_t i = 0; i < window.partition_by.size(); ++i) {
@@ -426,9 +442,10 @@ inline std::string output_name(const WindowCall& window) {
426442
}
427443
name += window_input_output_name(window.partition_by[i]);
428444
}
445+
wrote_clause = true;
429446
}
430447
if (!window.order_by.empty()) {
431-
if (!window.partition_by.empty()) {
448+
if (wrote_clause) {
432449
name += " ";
433450
}
434451
name += "ORDER BY ";
@@ -439,6 +456,13 @@ inline std::string output_name(const WindowCall& window) {
439456
name += window_input_output_name(window.order_by[i].expression);
440457
name += window.order_by[i].direction == SortDirection::Asc ? " ASC" : " DESC";
441458
}
459+
wrote_clause = true;
460+
}
461+
if (window.explicit_frame.has_value()) {
462+
if (wrote_clause) {
463+
name += " ";
464+
}
465+
name += window_frame_name(*window.explicit_frame);
442466
}
443467
name += ")";
444468
return name;

‎src/execution/interpreter.cpp‎

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,15 @@ std::int64_t checked_sum(std::int64_t left, std::int64_t right, const std::strin
599599
return result;
600600
}
601601

602+
void update_sum(AggregateValue& value, std::int64_t argument, const std::string& output_name) {
603+
if (!value.has_value) {
604+
value.sum = argument;
605+
value.has_value = true;
606+
return;
607+
}
608+
value.sum = checked_sum(value.sum, argument, output_name);
609+
}
610+
602611
Cell aggregate_argument_value(const plan::AggregateExpression& aggregate,
603612
const storage::ColumnarBatch& batch,
604613
std::size_t row,
@@ -631,12 +640,7 @@ void update_aggregate(AggregateValue& value,
631640
if (argument.is_null) {
632641
return;
633642
}
634-
if (!value.has_value) {
635-
value.sum = argument.int_value;
636-
value.has_value = true;
637-
return;
638-
}
639-
value.sum = checked_sum(value.sum, argument.int_value, aggregate.output_name);
643+
update_sum(value, argument.int_value, aggregate.output_name);
640644
return;
641645
}
642646
case sql::AggregateFunction::Min: {
@@ -1121,6 +1125,48 @@ std::vector<WindowPartition> build_window_partitions(const plan::WindowExpressio
11211125
return partitions;
11221126
}
11231127

1128+
std::vector<std::size_t> ordered_window_rows(const plan::WindowExpression& window,
1129+
const WindowPartition& partition,
1130+
const storage::ColumnarBatch& input,
1131+
ExecutionContext& context) {
1132+
auto ordered_rows = partition.rows;
1133+
std::stable_sort(ordered_rows.begin(), ordered_rows.end(), [&](std::size_t left, std::size_t right) {
1134+
return window_row_less(window.order_keys, input, left, right, context);
1135+
});
1136+
return ordered_rows;
1137+
}
1138+
1139+
void update_range_peer(AggregateValue& state,
1140+
const plan::AggregateExpression& aggregate,
1141+
const std::vector<std::size_t>& ordered_rows,
1142+
std::size_t begin,
1143+
std::size_t end,
1144+
const storage::ColumnarBatch& input,
1145+
ExecutionContext& context) {
1146+
if (aggregate.function != sql::AggregateFunction::Sum) {
1147+
for (std::size_t index = begin; index < end; ++index) {
1148+
update_aggregate(state, aggregate, input, ordered_rows[index], context);
1149+
}
1150+
return;
1151+
}
1152+
1153+
// RANGE peers share one observable frame boundary. Canonicalizing the
1154+
// peer's non-NULL SUM arguments makes checked intermediate overflow a
1155+
// function of the peer-keyed bag rather than incidental stable tie order.
1156+
std::vector<std::int64_t> arguments;
1157+
arguments.reserve(end - begin);
1158+
for (std::size_t index = begin; index < end; ++index) {
1159+
const auto argument = aggregate_argument_value(aggregate, input, ordered_rows[index], context);
1160+
if (!argument.is_null) {
1161+
arguments.push_back(argument.int_value);
1162+
}
1163+
}
1164+
std::sort(arguments.begin(), arguments.end());
1165+
for (const auto argument : arguments) {
1166+
update_sum(state, argument, aggregate.output_name);
1167+
}
1168+
}
1169+
11241170
OutputColumn evaluate_window(const plan::WindowExpression& window,
11251171
const storage::ColumnarBatch& input,
11261172
ExecutionContext& context) {
@@ -1135,10 +1181,7 @@ OutputColumn evaluate_window(const plan::WindowExpression& window,
11351181
case sql::WindowFunction::RowNumber:
11361182
case sql::WindowFunction::Rank:
11371183
case sql::WindowFunction::DenseRank: {
1138-
auto ordered_rows = partition.rows;
1139-
std::stable_sort(ordered_rows.begin(), ordered_rows.end(), [&](std::size_t left, std::size_t right) {
1140-
return window_row_less(window.order_keys, input, left, right, context);
1141-
});
1184+
const auto ordered_rows = ordered_window_rows(window, partition, input, context);
11421185

11431186
std::size_t rank = 1;
11441187
std::size_t dense_rank = 1;
@@ -1162,12 +1205,44 @@ OutputColumn evaluate_window(const plan::WindowExpression& window,
11621205
case sql::WindowFunction::Max: {
11631206
const auto aggregate = aggregate_expression_for_window(window);
11641207
AggregateValue state;
1165-
for (const auto row : partition.rows) {
1166-
update_aggregate(state, aggregate, input, row, context);
1208+
if (window.frame == sql::WindowFrame::WholePartition) {
1209+
for (const auto row : partition.rows) {
1210+
update_aggregate(state, aggregate, input, row, context);
1211+
}
1212+
const auto value = finalize_aggregate(state, aggregate);
1213+
for (const auto row : partition.rows) {
1214+
values.at(row) = value;
1215+
}
1216+
break;
11671217
}
1168-
const auto value = finalize_aggregate(state, aggregate);
1169-
for (const auto row : partition.rows) {
1170-
values.at(row) = value;
1218+
1219+
const auto ordered_rows = ordered_window_rows(window, partition, input, context);
1220+
if (window.frame == sql::WindowFrame::RowsCumulative) {
1221+
for (const auto row : ordered_rows) {
1222+
update_aggregate(state, aggregate, input, row, context);
1223+
values.at(row) = finalize_aggregate(state, aggregate);
1224+
}
1225+
break;
1226+
}
1227+
1228+
std::size_t peer_begin = 0;
1229+
while (peer_begin < ordered_rows.size()) {
1230+
auto peer_end = peer_begin + 1;
1231+
while (peer_end < ordered_rows.size() &&
1232+
window_rows_are_peers(window.order_keys,
1233+
input,
1234+
ordered_rows[peer_begin],
1235+
ordered_rows[peer_end],
1236+
context)) {
1237+
++peer_end;
1238+
}
1239+
update_range_peer(
1240+
state, aggregate, ordered_rows, peer_begin, peer_end, input, context);
1241+
const auto value = finalize_aggregate(state, aggregate);
1242+
for (auto index = peer_begin; index < peer_end; ++index) {
1243+
values.at(ordered_rows[index]) = value;
1244+
}
1245+
peer_begin = peer_end;
11711246
}
11721247
break;
11731248
}

0 commit comments

Comments
 (0)