[v1.5 backport] Fix: reset empty_range in the TIMESTAMP range() table function by wmTJc9IK0Q · Pull Request #23879 · duckdb/duckdb · GitHub
Skip to content

[v1.5 backport] Fix: reset empty_range in the TIMESTAMP range() table function - #23879

Merged
lnkuiper merged 1 commit into
duckdb:v1.5-variegatafrom
wmTJc9IK0Q:fix-range-empty-range
Jul 20, 2026
Merged

lnkuiper merged 1 commit into
duckdb:v1.5-variegatafrom
wmTJc9IK0Q:fix-range-empty-range

Conversation

@wmTJc9IK0Q

Copy link
Copy Markdown
Contributor

Backport of #23811 to v1.5

cc @lnkuiper


The TIMESTAMP/INTERVAL overload of the range() table function silently drops rows when used in a correlated LATERAL and at least one input row produces an empty range (start >= stop).

RangeDateTimeLocalState is reused across input rows, but GenerateRangeDateTimeParameters only ever SETS empty_range to true and never resets it to false. Once one row yields an empty range, the flag sticks and every subsequent row is treated as empty and emits nothing.

The integer overload (GenerateRangeParameters) already resets the flag, which is why only the timestamp overload was affected -- an identical LATERAL over range(BIGINT, BIGINT, BIGINT) returns the correct rows.

Reproduction (10 rows, only i=5 yields an empty range, so 9 rows are expected):

CREATE TABLE t AS
  SELECT i, TIMESTAMP '2024-01-01' + INTERVAL (i * 60) SECOND AS ts
  FROM generate_series(0, 9) s(i);

SELECT COUNT(*) FROM t, LATERAL range(
    ts + (CASE WHEN i = 5 THEN INTERVAL '7 minutes' ELSE INTERVAL '0 minutes' END),
    ts + INTERVAL '5 minutes',
    INTERVAL '10 minutes');
-- returns 1 (and the surviving row is i=3), expected 9

The corruption is not a truncation: which rows survive depends on where the empty range falls, so the result is wrong in a way that is hard to notice. The scalar range() (returning a LIST) is correct when unnested, so two spellings of the same query disagree.

Adds test/sql/table_function/range_timestamp_lateral_empty.test, which fails before this change and passes after.

Claude-Session: https://claude.ai/code/session_01CKmjtyYCWgo35X3ZCKaMYT

The TIMESTAMP/INTERVAL overload of the range() table function silently drops
rows when used in a correlated LATERAL and at least one input row produces an
empty range (start >= stop).

RangeDateTimeLocalState is reused across input rows, but
GenerateRangeDateTimeParameters only ever SETS empty_range to true and never
resets it to false. Once one row yields an empty range, the flag sticks and every
subsequent row is treated as empty and emits nothing.

The integer overload (GenerateRangeParameters) already resets the flag, which is
why only the timestamp overload was affected -- an identical LATERAL over
range(BIGINT, BIGINT, BIGINT) returns the correct rows.

Reproduction (10 rows, only i=5 yields an empty range, so 9 rows are expected):

    CREATE TABLE t AS
      SELECT i, TIMESTAMP '2024-01-01' + INTERVAL (i * 60) SECOND AS ts
      FROM generate_series(0, 9) s(i);

    SELECT COUNT(*) FROM t, LATERAL range(
        ts + (CASE WHEN i = 5 THEN INTERVAL '7 minutes' ELSE INTERVAL '0 minutes' END),
        ts + INTERVAL '5 minutes',
        INTERVAL '10 minutes');
    -- returns 1 (and the surviving row is i=3), expected 9

The corruption is not a truncation: which rows survive depends on where the empty
range falls, so the result is wrong in a way that is hard to notice. The scalar
range() (returning a LIST) is correct when unnested, so two spellings of the same
query disagree.

Adds test/sql/table_function/range_timestamp_lateral_empty.test, which fails
before this change and passes after.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKmjtyYCWgo35X3ZCKaMYT
@duckdb-draftbot
duckdb-draftbot marked this pull request as draft July 16, 2026 16:53
@lnkuiper
lnkuiper marked this pull request as ready for review July 17, 2026 07:40
@lnkuiper
lnkuiper merged commit 4a11fb2 into duckdb:v1.5-variegata Jul 20, 2026
215 of 270 checks passed
@wmTJc9IK0Q
wmTJc9IK0Q deleted the fix-range-empty-range branch July 20, 2026 17:59
kaka11chen pushed a commit to kaka11chen/astrovela_vane that referenced this pull request Aug 14, 2026
… function (duckdb/duckdb#23879)

Backport of #23811 to v1.5

cc @lnkuiper 

---

The TIMESTAMP/INTERVAL overload of the range() table function silently
drops rows when used in a correlated LATERAL and at least one input row
produces an empty range (start >= stop).

RangeDateTimeLocalState is reused across input rows, but
GenerateRangeDateTimeParameters only ever SETS empty_range to true and
never resets it to false. Once one row yields an empty range, the flag
sticks and every subsequent row is treated as empty and emits nothing.

The integer overload (GenerateRangeParameters) already resets the flag,
which is why only the timestamp overload was affected -- an identical
LATERAL over range(BIGINT, BIGINT, BIGINT) returns the correct rows.

Reproduction (10 rows, only i=5 yields an empty range, so 9 rows are
expected):

    CREATE TABLE t AS
      SELECT i, TIMESTAMP '2024-01-01' + INTERVAL (i * 60) SECOND AS ts
      FROM generate_series(0, 9) s(i);

    SELECT COUNT(*) FROM t, LATERAL range(
ts + (CASE WHEN i = 5 THEN INTERVAL '7 minutes' ELSE INTERVAL '0
minutes' END),
        ts + INTERVAL '5 minutes',
        INTERVAL '10 minutes');
    -- returns 1 (and the surviving row is i=3), expected 9

The corruption is not a truncation: which rows survive depends on where
the empty range falls, so the result is wrong in a way that is hard to
notice. The scalar range() (returning a LIST) is correct when unnested,
so two spellings of the same query disagree.

Adds test/sql/table_function/range_timestamp_lateral_empty.test, which
fails before this change and passes after.


Claude-Session: https://claude.ai/code/session_01CKmjtyYCWgo35X3ZCKaMYT

Upstream-Commit: duckdb/duckdb@4a11fb2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants