| -- Licensed to the Apache Software Foundation (ASF) under one |
| -- or more contributor license agreements. See the NOTICE file |
| -- distributed with this work for additional information |
| -- regarding copyright ownership. The ASF licenses this file |
| -- to you under the Apache License, Version 2.0 (the |
| -- "License"); you may not use this file except in compliance |
| -- with the License. You may obtain a copy of the License at |
| -- |
| -- http://www.apache.org/licenses/LICENSE-2.0 |
| -- |
| -- Unless required by applicable law or agreed to in writing, |
| -- software distributed under the License is distributed on an |
| -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| -- KIND, either express or implied. See the License for the |
| -- specific language governing permissions and limitations |
| -- under the License. |
| |
| -- ============================================================ |
| -- Setup: shared tables |
| -- ============================================================ |
| |
| statement |
| CREATE TABLE emp(dept string, id int, salary int, hire_yr int) USING parquet |
| |
| statement |
| INSERT INTO emp VALUES |
| ('eng', 1, 100, 2020), |
| ('eng', 2, 100, 2021), |
| ('eng', 2, 100, 2021), |
| ('eng', 3, 150, 2022), |
| ('eng', 4, NULL, 2023), |
| ('eng', 5, 200, 2024), |
| ('sales', 6, 90, 2020), |
| ('sales', 7, 90, 2021), |
| ('sales', 8, NULL, 2022), |
| ('sales', 9, 110, 2023), |
| ('sales', 10, 120, 2024), |
| ('ops', 11, NULL, 2020), |
| ('ops', 11, NULL, 2020), |
| ('ops', 12, NULL, 2021), |
| ('ops', 13, 50, 2022) |
| |
| statement |
| CREATE TABLE emp_allnull(dept string, id int, salary int) USING parquet |
| |
| statement |
| INSERT INTO emp_allnull VALUES |
| ('a', 1, NULL), |
| ('a', 2, NULL), |
| ('b', 3, NULL), |
| ('b', 4, NULL) |
| |
| statement |
| CREATE TABLE daily(dt string, amt int) USING parquet |
| |
| statement |
| INSERT INTO daily VALUES |
| ('2024-01-01', 10), |
| ('2024-01-02', 20), |
| ('2024-01-03', 30), |
| ('2024-01-04', 40) |
| |
| statement |
| CREATE TABLE scores(player string, game int, score int) USING parquet |
| |
| statement |
| INSERT INTO scores VALUES |
| ('alice', 1, 10), |
| ('alice', 2, 10), |
| ('alice', 3, 20), |
| ('alice', 4, 30), |
| ('bob', 1, 10), |
| ('bob', 2, 10), |
| ('bob', 3, 10), |
| ('bob', 4, 40), |
| ('carol', 1, 50), |
| ('carol', 2, 50), |
| ('carol', 3, 50), |
| ('carol', 4, 50) |
| |
| statement |
| CREATE TABLE date_events(val_date date, cate string) USING parquet |
| |
| statement |
| INSERT INTO date_events VALUES |
| (DATE '2017-08-01', 'a'), |
| (DATE '2017-08-01', 'a'), |
| (DATE '2017-08-02', 'a'), |
| (DATE '2020-12-31', 'a'), |
| (DATE '2017-08-01', 'b'), |
| (DATE '2017-08-03', 'b'), |
| (DATE '2020-12-31', 'b'), |
| (NULL, NULL), |
| (DATE '2017-08-01', NULL) |
| |
| statement |
| CREATE TABLE t_single(val int) USING parquet |
| |
| statement |
| INSERT INTO t_single VALUES (1) |
| |
| -- ############################################################ |
| -- Section 1: Basic window combinations |
| -- ############################################################ |
| |
| -- ============================================================ |
| -- 1.1: aggregate over unbounded window (no ORDER BY) |
| -- ============================================================ |
| |
| query |
| SELECT id, dept, salary, |
| COUNT(*) OVER () AS c, |
| SUM(salary) OVER () AS s, |
| MAX(salary) OVER () AS mx, |
| MIN(salary) OVER () AS mn |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.2: aggregate with ORDER BY (running aggregates) |
| -- ============================================================ |
| |
| query |
| SELECT id, dept, salary, |
| COUNT(*) OVER (ORDER BY id) AS c, |
| SUM(salary) OVER (ORDER BY id) AS s, |
| MAX(salary) OVER (ORDER BY id) AS mx, |
| MIN(salary) OVER (ORDER BY id) AS mn |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.3: aggregate with PARTITION BY + ORDER BY |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| COUNT(*) OVER (PARTITION BY dept ORDER BY id) AS c, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id) AS s, |
| AVG(salary) OVER (PARTITION BY dept ORDER BY id) AS a |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.4: aggregate with ORDER BY DESC |
| -- ============================================================ |
| |
| query |
| SELECT id, dept, salary, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id DESC) AS s |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.5: ranking functions (ROW_NUMBER, RANK, DENSE_RANK) |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| ROW_NUMBER() OVER (PARTITION BY dept ORDER BY id) AS rn, |
| RANK() OVER (PARTITION BY dept ORDER BY id) AS rk, |
| DENSE_RANK() OVER (PARTITION BY dept ORDER BY id) AS drk |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.6: PERCENT_RANK / CUME_DIST / NTILE |
| -- NTILE divergence tends to surface when partition size is not |
| -- evenly divisible by the bucket count. The 'eng' partition (6 rows |
| -- with NTILE(4)) is the previously-buggy case from DataFusion #22049 |
| -- (fixed by #22051 in DataFusion 54.0.0): the old i*n/num_rows |
| -- formula yielded bucket sizes 2,1,2,1 instead of 2,2,1,1. |
| -- ============================================================ |
| |
| query tolerance=1e-6 |
| SELECT dept, id, salary, |
| PERCENT_RANK() OVER (PARTITION BY dept ORDER BY id) AS pr, |
| CUME_DIST() OVER (PARTITION BY dept ORDER BY id) AS cd, |
| NTILE(4) OVER (PARTITION BY dept ORDER BY id) AS bucket |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.7: LAG / LEAD with default offset |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| LAG(salary) OVER (PARTITION BY dept ORDER BY id) AS lg, |
| LEAD(salary) OVER (PARTITION BY dept ORDER BY id) AS ld |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.8: LAG / LEAD with explicit offset and default value |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| LAG(salary, 2, -1) OVER (PARTITION BY dept ORDER BY id) AS lg2, |
| LEAD(salary, 2, -1) OVER (PARTITION BY dept ORDER BY id) AS ld2 |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.9: ROWS BETWEEN frames |
| -- Note: Spark parses `N PRECEDING` in ROWS frames as UnaryMinus(Literal(N)), |
| -- which only becomes a plain Literal(-N) after ConstantFolding runs. |
| -- CometSqlFileTestSuite disables ConstantFolding, so the ROWS frames below |
| -- only use bounds that parse directly (UNBOUNDED / CURRENT ROW / N FOLLOWING). |
| -- N PRECEDING in ROWS frames is covered by CometWindowExecSuite via the |
| -- DataFrame API, which emits Literal(-N) without needing the optimizer. |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS s_run, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) AS s_lookahead, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING) AS s_window3, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS s_tail |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.10: RANGE BETWEEN with numeric ORDER BY |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id |
| RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS s_rng |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.11: RANGE BETWEEN with DATE ORDER BY |
| -- Spark interprets the integer frame offset N on a DATE ORDER BY column as |
| -- N days, so MAX(val_date) OVER (... RANGE BETWEEN CURRENT ROW AND 2 FOLLOWING) |
| -- returns the largest val_date within the next 2 days of each row. |
| -- Mirrors the "max(val_date) ... RANGE BETWEEN CURRENT ROW AND 2 FOLLOWING" |
| -- case from Spark's window.sql. Comet falls back because its native window |
| -- planner ships RANGE frame offsets as Int64 while arrow-arith requires an |
| -- Interval RHS for Date32 arithmetic. Once the native planner coerces Int -> |
| -- Interval for DATE sort keys (as DataFusion's type-coercion analyzer does), |
| -- the guard in CometWindowExec can be removed and this test will start |
| -- failing because Comet stops falling back — that's the signal to re-enable |
| -- native execution. |
| -- ============================================================ |
| |
| query expect_fallback(RANGE frame with explicit offset on DATE ORDER BY is not supported) |
| SELECT val_date, cate, |
| MAX(val_date) OVER (PARTITION BY cate ORDER BY val_date |
| RANGE BETWEEN CURRENT ROW AND 2 FOLLOWING) AS mx_d |
| FROM date_events |
| ORDER BY cate, val_date |
| |
| -- ============================================================ |
| -- 1.12: RANGE BETWEEN with DECIMAL ORDER BY DESC |
| -- Ported from Spark's typeCoercion/native/windowFrameCoercion.sql: |
| -- SELECT COUNT(*) OVER (PARTITION BY 1 ORDER BY cast(1 as decimal(10, 0)) DESC |
| -- RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t |
| -- Comet falls back because Spark decimal arithmetic widens precision on |
| -- +/-, so the native frame-boundary arithmetic produces e.g. Decimal(11,0) |
| -- while the current row stays Decimal(10,0), and DataFusion's comparator |
| -- fails with "Uncomparable values". Once the native planner preserves the |
| -- ORDER BY column's precision when computing RANGE boundaries, the guard |
| -- in CometWindowExec can be removed and this test will start failing |
| -- because Comet stops falling back — that's the signal to re-enable it. |
| -- ============================================================ |
| |
| query expect_fallback(RANGE frame with explicit offset on DECIMAL ORDER BY is not supported) |
| SELECT COUNT(*) OVER (PARTITION BY 1 |
| ORDER BY CAST(1 AS DECIMAL(10, 0)) DESC |
| RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) AS c |
| FROM t_single |
| |
| -- ============================================================ |
| -- 1.13: RANGE frame with BIGINT ORDER BY values near Long.MaxValue |
| -- Ported from Spark's postgreSQL/window_part2.sql: |
| -- select x.id, last(x.id) over (order by x.id range between current row and 4 following) |
| -- from range(9223372036854775804, 9223372036854775807) x; |
| -- When Spark computes the frame upper bound `current + 4` for a row near |
| -- Long.MaxValue the non-ANSI Add wraps to a negative value, the wrapped |
| -- upper bound ends up below the current row, the range frame is empty, |
| -- and LAST_VALUE returns NULL. Comet's native RANGE frame doesn't |
| -- reproduce that wrap, and Spark's SlidingWindowFunctionFrame iterator |
| -- state (rows excluded by an earlier wrapped bound stay excluded) can't |
| -- be matched stateless on the native side, so Comet falls back to Spark |
| -- for FIRST_VALUE / LAST_VALUE on RANGE with literal offsets. |
| -- ============================================================ |
| |
| statement |
| CREATE TABLE bigint_max_range(id bigint) USING parquet |
| |
| statement |
| INSERT INTO bigint_max_range VALUES |
| (9223372036854775804), |
| (9223372036854775805), |
| (9223372036854775806) |
| |
| query expect_fallback(LAST_VALUE with RANGE frame and literal offset is not supported) |
| SELECT id, |
| LAST_VALUE(id) OVER (ORDER BY id |
| RANGE BETWEEN CURRENT ROW AND 4 FOLLOWING) AS lv |
| FROM bigint_max_range |
| |
| -- ============================================================ |
| -- 1.14: RANGE frame with BIGINT ORDER BY values near Long.MinValue |
| -- Companion to 1.13 covering the underflow side. Spark wraps the lower |
| -- bound `current - 4` for rows near Long.MinValue, the wrapped bound |
| -- ends up above the current row, and FIRST_VALUE returns NULL. Same |
| -- fallback reason as 1.13. |
| -- ============================================================ |
| |
| statement |
| CREATE TABLE bigint_min_range(id bigint) USING parquet |
| |
| statement |
| INSERT INTO bigint_min_range VALUES |
| (-9223372036854775807), |
| (-9223372036854775806), |
| (-9223372036854775805) |
| |
| query expect_fallback(FIRST_VALUE with RANGE frame and literal offset is not supported) |
| SELECT id, |
| FIRST_VALUE(id) OVER (ORDER BY id |
| RANGE BETWEEN 4 PRECEDING AND CURRENT ROW) AS fv |
| FROM bigint_min_range |
| |
| -- ============================================================ |
| -- 1.15: multiple PARTITION BY + multiple ORDER BY |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, hire_yr, salary, |
| SUM(salary) OVER (PARTITION BY dept, hire_yr ORDER BY id) AS s1, |
| RANK() OVER (PARTITION BY dept ORDER BY hire_yr, id) AS rk |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.16: complex expression in aggregate input |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| SUM(salary + 10) OVER (PARTITION BY dept ORDER BY id) AS s_plus_10 |
| FROM emp |
| |
| -- ============================================================ |
| -- 1.17: multiple window functions with mixed specs in one query |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| ROW_NUMBER() OVER (PARTITION BY dept ORDER BY id) AS rn, |
| RANK() OVER (PARTITION BY dept ORDER BY salary) AS rk_salary, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id) AS run_sum, |
| MAX(salary) OVER () AS global_max |
| FROM emp |
| |
| -- ############################################################ |
| -- Section 2: Mixed nulls |
| -- ############################################################ |
| |
| -- ============================================================ |
| -- 2.1: COUNT(*) vs COUNT(col) with nulls |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| COUNT(*) OVER (PARTITION BY dept ORDER BY id) AS c_star, |
| COUNT(salary) OVER (PARTITION BY dept ORDER BY id) AS c_col |
| FROM emp |
| |
| -- ============================================================ |
| -- 2.2: SUM / AVG / MIN / MAX skip nulls |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id) AS s, |
| AVG(salary) OVER (PARTITION BY dept ORDER BY id) AS a, |
| MIN(salary) OVER (PARTITION BY dept ORDER BY id) AS mn, |
| MAX(salary) OVER (PARTITION BY dept ORDER BY id) AS mx |
| FROM emp |
| |
| -- ============================================================ |
| -- 2.3: LAG / LEAD RESPECT NULLS (default) vs IGNORE NULLS |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| LAG(salary) OVER (PARTITION BY dept ORDER BY id) AS lag_respect, |
| LAG(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id) AS lag_ignore, |
| LEAD(salary) OVER (PARTITION BY dept ORDER BY id) AS lead_respect, |
| LEAD(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id) AS lead_ignore |
| FROM emp |
| |
| -- ============================================================ |
| -- 2.4: FIRST_VALUE / LAST_VALUE RESPECT vs IGNORE NULLS |
| -- ============================================================ |
| query |
| SELECT dept, id, salary, |
| FIRST_VALUE(salary) OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS fv_respect, |
| FIRST_VALUE(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS fv_ignore, |
| LAST_VALUE(salary) OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lv_respect, |
| LAST_VALUE(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lv_ignore |
| FROM emp |
| |
| -- ============================================================ |
| -- 2.5: all-null partition |
| -- ============================================================ |
| query |
| SELECT dept, id, salary, |
| SUM(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS s, |
| AVG(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS a, |
| COUNT(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS c_col, |
| FIRST_VALUE(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS fv_ig |
| FROM emp_allnull |
| |
| -- ============================================================ |
| -- 2.6: NTH_VALUE with nulls RESPECTed vs IGNORED |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| NTH_VALUE(salary, 2) OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS nth_respect, |
| NTH_VALUE(salary, 2) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS nth_ignore |
| FROM emp |
| |
| query |
| SELECT dept, id, salary, |
| first_value(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS fv_ignore, |
| last_value(salary) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS lv_ignore, |
| nth_value(salary, 1) IGNORE NULLS OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS nth_ignore |
| FROM emp |
| |
| -- ============================================================ |
| -- 2.7: AVG with ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING |
| -- over trailing NULLs (inverse transition). |
| -- Ported from Spark's postgreSQL/window_part4.sql: |
| -- SELECT i, AVG(v) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) |
| -- FROM (VALUES (1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); |
| -- The lower bound (CurrentRow) means the frame is *not* ever-expanding, |
| -- so this routes through Comet's sliding-window branch (DataFusion's |
| -- retract-capable `avg`). Pre-DataFusion 54.0.0 that path returned NaN |
| -- once every contributing value had been retracted (DataFusion #22138); |
| -- the fix in DataFusion #22139 short-circuits `count == 0` to NULL. |
| -- The expected result 1.5, 2.0, NULL, NULL pins that fix. |
| -- ============================================================ |
| |
| statement |
| CREATE TABLE avg_nulls_trailing(i int, v int) USING parquet |
| |
| statement |
| INSERT INTO avg_nulls_trailing VALUES |
| (1, 1), |
| (2, 2), |
| (3, NULL), |
| (4, NULL) |
| |
| query |
| SELECT i, |
| AVG(v) OVER (ORDER BY i |
| ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS a |
| FROM avg_nulls_trailing |
| |
| -- ############################################################ |
| -- Section 3: Window functions with GROUP BY |
| -- ############################################################ |
| |
| -- ============================================================ |
| -- 3.1: window over aggregated rows (window in outer SELECT) |
| -- ============================================================ |
| |
| query |
| SELECT dept, total_salary, |
| RANK() OVER (ORDER BY total_salary DESC) AS rk |
| FROM ( |
| SELECT dept, SUM(salary) AS total_salary |
| FROM emp |
| GROUP BY dept |
| ) t |
| |
| -- ============================================================ |
| -- 3.2: window and GROUP BY in the same query (aggregated input to window) |
| -- ============================================================ |
| |
| query |
| SELECT dept, |
| SUM(salary) AS dept_total, |
| SUM(SUM(salary)) OVER () AS grand_total, |
| SUM(salary) / SUM(SUM(salary)) OVER () AS share |
| FROM emp |
| GROUP BY dept |
| |
| -- ============================================================ |
| -- 3.3: window with GROUP BY over multiple keys |
| -- ============================================================ |
| |
| query |
| SELECT dept, hire_yr, |
| SUM(salary) AS yr_total, |
| SUM(SUM(salary)) OVER (PARTITION BY dept ORDER BY hire_yr) AS dept_running |
| FROM emp |
| WHERE salary IS NOT NULL |
| GROUP BY dept, hire_yr |
| |
| -- ============================================================ |
| -- 3.4: GROUP BY producing a single row per group, RANK compares groups |
| -- ============================================================ |
| |
| query |
| SELECT dept, cnt, avg_sal, |
| DENSE_RANK() OVER (ORDER BY avg_sal DESC) AS rk_by_avg |
| FROM ( |
| SELECT dept, COUNT(*) AS cnt, AVG(salary) AS avg_sal |
| FROM emp |
| GROUP BY dept |
| ) t |
| |
| -- ############################################################ |
| -- Section 4: LAST_VALUE default frame semantics |
| -- ############################################################ |
| -- SQL default frame when ORDER BY is present is |
| -- RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
| -- so LAST_VALUE without an explicit frame returns the current row's value, |
| -- NOT the last row of the partition. Most users intuitively expect the |
| -- latter; the explicit ROWS BETWEEN ... UNBOUNDED FOLLOWING form gives it. |
| |
| -- ============================================================ |
| -- 4.1: LAST_VALUE default frame == current row value |
| -- ============================================================ |
| |
| query |
| SELECT dt, amt, |
| LAST_VALUE(amt) OVER (ORDER BY dt) AS lv_default |
| FROM daily |
| |
| -- ============================================================ |
| -- 4.2: LAST_VALUE explicit ROWS BETWEEN ... UNBOUNDED FOLLOWING |
| -- ============================================================ |
| |
| query |
| SELECT dt, amt, |
| LAST_VALUE(amt) OVER (ORDER BY dt |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lv_full |
| FROM daily |
| |
| -- ============================================================ |
| -- 4.3: contrast default vs explicit FIRST_VALUE / LAST_VALUE |
| -- ============================================================ |
| |
| query |
| SELECT dt, amt, |
| FIRST_VALUE(amt) OVER (ORDER BY dt) AS fv_default, |
| FIRST_VALUE(amt) OVER (ORDER BY dt |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS fv_full, |
| LAST_VALUE(amt) OVER (ORDER BY dt) AS lv_default, |
| LAST_VALUE(amt) OVER (ORDER BY dt |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lv_full |
| FROM daily |
| |
| -- ============================================================ |
| -- 4.4: LAST_VALUE default with PARTITION BY |
| -- ============================================================ |
| |
| query |
| SELECT dept, id, salary, |
| LAST_VALUE(salary) OVER (PARTITION BY dept ORDER BY id) AS lv_default, |
| LAST_VALUE(salary) OVER (PARTITION BY dept ORDER BY id |
| ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lv_full |
| FROM emp |
| |
| -- ============================================================ |
| -- 4.5: LAST_VALUE default RANGE semantics over peer groups |
| -- Peer rows (same ORDER BY key) see each other's value in the default |
| -- RANGE frame, so LAST_VALUE within a peer group returns the last peer's |
| -- value rather than just the current row. Returning `score` (the ORDER BY |
| -- column) keeps the output deterministic since all peers in a peer group |
| -- share the same score by definition. |
| -- ============================================================ |
| |
| query |
| SELECT player, game, score, |
| LAST_VALUE(score) OVER (PARTITION BY player ORDER BY score) AS lv_default, |
| FIRST_VALUE(score) OVER (PARTITION BY player ORDER BY score) AS fv_default |
| FROM scores |
| |
| -- ############################################################ |
| -- Section 5: Peer groups with duplicates |
| -- ############################################################ |
| -- Peer groups = rows that share the same ORDER BY key within a partition. |
| -- RANK assigns the same rank to peers and then skips (1,1,3). DENSE_RANK |
| -- assigns the same rank to peers without skipping (1,1,2). ROW_NUMBER |
| -- breaks ties arbitrarily but deterministically within a single plan. |
| |
| -- ============================================================ |
| -- 5.1: RANK vs DENSE_RANK vs ROW_NUMBER with ties |
| -- ROW_NUMBER needs a tie-breaker (game) to be deterministic — tied rows |
| -- can get row-numbers in either order otherwise. RANK and DENSE_RANK |
| -- give peers the same rank regardless of peer order, so they stay on |
| -- the tied `ORDER BY score` to demonstrate tie handling. |
| -- ============================================================ |
| |
| query |
| SELECT player, game, score, |
| ROW_NUMBER() OVER (PARTITION BY player ORDER BY score, game) AS rn, |
| RANK() OVER (PARTITION BY player ORDER BY score) AS rk, |
| DENSE_RANK() OVER (PARTITION BY player ORDER BY score) AS drk |
| FROM scores |
| |
| -- ============================================================ |
| -- 5.2: PERCENT_RANK and CUME_DIST across peer groups |
| -- CUME_DIST returns (#rows with order_by <= current) / (#rows in partition), |
| -- so every peer gets the same CUME_DIST value. PERCENT_RANK uses (rank-1)/(n-1), |
| -- so peers share a PERCENT_RANK as well. |
| -- ============================================================ |
| |
| query tolerance=1e-6 |
| SELECT player, game, score, |
| PERCENT_RANK() OVER (PARTITION BY player ORDER BY score) AS pr, |
| CUME_DIST() OVER (PARTITION BY player ORDER BY score) AS cd |
| FROM scores |
| |
| -- ============================================================ |
| -- 5.3: peer group where all rows share the same ORDER BY key |
| -- Every row is a peer of every other, so RANK / DENSE_RANK = 1 for all |
| -- and CUME_DIST = 1 for all. |
| -- ============================================================ |
| |
| query tolerance=1e-6 |
| SELECT player, game, score, |
| RANK() OVER (PARTITION BY player ORDER BY score) AS rk, |
| DENSE_RANK() OVER (PARTITION BY player ORDER BY score) AS drk, |
| CUME_DIST() OVER (PARTITION BY player ORDER BY score) AS cd, |
| PERCENT_RANK() OVER (PARTITION BY player ORDER BY score) AS pr |
| FROM scores |
| WHERE player = 'carol' |
| |
| -- ============================================================ |
| -- 5.4: SUM / COUNT default RANGE frame over peer groups |
| -- Default frame (RANGE UNBOUNDED PRECEDING TO CURRENT ROW) includes all |
| -- peers, so running SUM jumps by the peer-group total at each peer |
| -- rather than row-by-row. |
| -- ============================================================ |
| |
| query |
| SELECT player, game, score, |
| SUM(score) OVER (PARTITION BY player ORDER BY score) AS run_sum, |
| COUNT(score) OVER (PARTITION BY player ORDER BY score) AS run_count |
| FROM scores |
| |
| -- ============================================================ |
| -- 5.5: ROWS frame vs default RANGE frame over peer groups |
| -- ROWS frame is position-based: running SUM moves row-by-row ignoring peers. |
| -- The ROWS ordering adds `game` as a tie-breaker to make per-row output |
| -- deterministic (ROWS with tied ORDER BY can assign partial sums to peers |
| -- in either order). RANGE intentionally keeps ORDER BY score only, since |
| -- peer rows share the frame and therefore produce identical partial sums |
| -- regardless of intra-peer order. |
| -- ============================================================ |
| |
| query |
| SELECT player, game, score, |
| SUM(score) OVER (PARTITION BY player ORDER BY score, game |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_sum, |
| SUM(score) OVER (PARTITION BY player ORDER BY score |
| RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS range_sum |
| FROM scores |
| |
| -- ############################################################ |
| -- Section 6: Other window / aggregate functions |
| -- ############################################################ |
| -- Aggregate functions beyond Comet's native window-aggregate support |
| -- (Count/Min/Max/Sum/Average/First/Last). Everything below falls back to |
| -- Spark because Comet's native WindowExec serde does not recognise the |
| -- aggregate. |
| |
| -- ============================================================ |
| -- 6.1: statistical aggregates over a window (STDDEV / VAR / SKEW / KURT) |
| -- ============================================================ |
| |
| query expect_fallback(is not supported for window function) |
| SELECT dept, id, salary, |
| STDDEV_POP(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS sd_pop, |
| STDDEV_SAMP(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS sd_samp, |
| VAR_POP(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS v_pop, |
| VAR_SAMP(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS v_samp, |
| SKEWNESS(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS skew, |
| KURTOSIS(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS kurt |
| FROM emp |
| |
| -- ============================================================ |
| -- 6.2: collection aggregates (COLLECT_LIST) |
| -- id is unique per partition, so the ORDER BY makes COLLECT_LIST insertion |
| -- order deterministic. |
| -- ============================================================ |
| |
| query expect_fallback(is not supported for window function) |
| SELECT dept, id, salary, |
| COLLECT_LIST(salary) OVER (PARTITION BY dept ORDER BY id, salary |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS so_far |
| FROM emp |
| |
| -- ============================================================ |
| -- 6.3: bitwise aggregates (BIT_AND / BIT_OR / BIT_XOR) |
| -- ============================================================ |
| |
| query expect_fallback(is not supported for window function) |
| SELECT dept, id, salary, |
| BIT_AND(salary) OVER (PARTITION BY dept) AS b_and, |
| BIT_OR(salary) OVER (PARTITION BY dept) AS b_or, |
| BIT_XOR(salary) OVER (PARTITION BY dept) AS b_xor |
| FROM emp |
| |
| -- ============================================================ |
| -- 6.4: correlation / covariance (CORR / COVAR_POP / COVAR_SAMP) |
| -- ============================================================ |
| |
| query expect_fallback(is not supported for window function) |
| SELECT dept, id, salary, hire_yr, |
| CORR(salary, hire_yr) OVER (PARTITION BY dept) AS corr, |
| COVAR_POP(salary, hire_yr) OVER (PARTITION BY dept) AS covp, |
| COVAR_SAMP(salary, hire_yr) OVER (PARTITION BY dept) AS covs |
| FROM emp |
| |
| -- ============================================================ |
| -- 6.5: percentile aggregates (PERCENTILE / MEDIAN / APPROX_PERCENTILE) |
| -- ============================================================ |
| |
| query expect_fallback(is not supported for window function) |
| SELECT dept, id, salary, |
| PERCENTILE(salary, 0.5) OVER (PARTITION BY dept) AS p50, |
| MEDIAN(salary) OVER (PARTITION BY dept) AS med, |
| APPROX_PERCENTILE(salary, 0.5) OVER (PARTITION BY dept) AS app_p50 |
| FROM emp |
| |
| -- ============================================================ |
| -- 6.6: value-selection aggregates (ANY_VALUE / MAX_BY / MIN_BY / MODE) |
| -- ============================================================ |
| |
| query expect_fallback(is not supported for window function) |
| SELECT dept, id, salary, |
| ANY_VALUE(salary) OVER (PARTITION BY dept ORDER BY id, salary) AS anyv, |
| MAX_BY(id, salary) OVER (PARTITION BY dept) AS max_id_by_salary, |
| MIN_BY(id, salary) OVER (PARTITION BY dept) AS min_id_by_salary, |
| MODE(salary) OVER (PARTITION BY dept) AS mode_salary |
| FROM emp |
| |
| -- ############################################################ |
| -- Section 7: LAG / LEAD respect null values (mirrors Spark |
| -- SQLWindowFunctionSuite "lead/lag should respect null values") |
| -- ############################################################ |
| |
| statement |
| CREATE TABLE lag_lead_nulls(a int, b int, c int) USING parquet |
| |
| statement |
| INSERT INTO lag_lead_nulls VALUES |
| (CAST(NULL AS INT), 1, 3), |
| (CAST(NULL AS INT), 2, 4) |
| |
| -- ============================================================ |
| -- 7.1: LAG / LEAD with literal default — input is null but the |
| -- default still fires when the offset row does not exist |
| -- (RESPECT NULLS is the default behaviour). |
| -- ============================================================ |
| |
| query |
| SELECT |
| b, |
| LAG(a, 1, 321) OVER (ORDER BY b) AS lg, |
| LEAD(a, 1, 321) OVER (ORDER BY b) AS ld |
| FROM lag_lead_nulls |
| |
| -- ============================================================ |
| -- 7.2: LAG / LEAD with a non-literal (column-reference) default |
| -- falls back to Spark — Comet's native lag/lead only accepts a |
| -- literal default value. |
| -- ============================================================ |
| |
| query expect_fallback(default value must be a literal) |
| SELECT |
| b, |
| LAG(a, 1, c) OVER (ORDER BY b) AS lg, |
| LEAD(a, 1, c) OVER (ORDER BY b) AS ld |
| FROM lag_lead_nulls |
| |
| -- ############################################################ |
| -- Section 8: decimal aggregate overflow semantics |
| -- ############################################################ |
| -- Spark returns NULL when a decimal SUM overflows the result precision under |
| -- non-ANSI semantics. Comet's native window path enforces this for |
| -- ever-expanding frames (lower bound UNBOUNDED PRECEDING) via its |
| -- overflow-aware SumDecimal UDAF. Sliding frames would route to DataFusion's |
| -- built-in sum, whose accumulator wraps on overflow instead of returning NULL, |
| -- so Comet falls back to Spark for sliding decimal SUM / AVG (#4729). |
| |
| statement |
| CREATE TABLE dec_overflow(g int, v decimal(38,0)) USING parquet |
| |
| statement |
| INSERT INTO dec_overflow VALUES |
| (1, 90000000000000000000000000000000000000), |
| (1, 90000000000000000000000000000000000000), |
| (1, 90000000000000000000000000000000000000) |
| |
| -- ============================================================ |
| -- 8.1: ever-expanding decimal SUM that overflows the result precision |
| -- returns NULL (Comet's SumDecimal UDAF matches Spark's overflow-to-NULL). |
| -- ============================================================ |
| |
| query |
| SELECT v, |
| SUM(v) OVER (PARTITION BY g ORDER BY v |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS run_sum |
| FROM dec_overflow |
| |
| -- ============================================================ |
| -- 8.2: sliding decimal SUM falls back to Spark. DataFusion's built-in sum |
| -- wraps on overflow (producing an out-of-range Decimal128) rather than |
| -- returning Spark's NULL, and overflow can't be detected at plan time, so the |
| -- whole sliding decimal SUM case falls back. Re-enable native execution when |
| -- the native window path enforces Spark decimal overflow semantics for |
| -- sliding frames. |
| -- ============================================================ |
| |
| query expect_fallback(SUM on DECIMAL with a sliding window frame is not supported) |
| SELECT v, |
| SUM(v) OVER (PARTITION BY g ORDER BY v |
| ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS tail_sum |
| FROM dec_overflow |