Skip to content

fix(buzz-db): return an empty page from get_channel_window when limit is 0 - #3536

Open
hasky00 wants to merge 1 commit into
block:mainfrom
hasky00:fix/channel-window-zero-limit
Open

fix(buzz-db): return an empty page from get_channel_window when limit is 0#3536
hasky00 wants to merge 1 commit into
block:mainfrom
hasky00:fix/channel-window-zero-limit

Conversation

@hasky00

@hasky00 hasky00 commented Jul 29, 2026

Copy link
Copy Markdown

Summary

get_channel_window in buzz-db can violate its own documented pagination
contract when called with limit == 0.

ChannelWindow::next_cursor is documented as Some iff has_more
(crates/buzz-db/src/thread.rs). The function derives has_more from a
limit + 1 probe row before truncation, but derives next_cursor from the
truncated row set:

q = q.bind(limit as i64 + 1); // limit == 0 → binds 1, fetches the probe row
let mut db_rows = q.fetch_all(pool).await?;
let has_more = db_rows.len() > limit as usize; // 1 > 0 → true
db_rows.truncate(limit as usize);              // → 0 rows
let next_cursor = if has_more {
    match db_rows.last() { Some(row) => …, None => None } // empty → None
} else { None };

With limit == 0 and at least one matching row, this returns
has_more = true and next_cursor = None, breaking the invariant. A
caller that trusts the contract (window.next_cursor.expect("has_more implies next_cursor") — exactly what the module's own pagination test does) panics,
and a keyset-pagination loop would spin forever returning empty pages.

The sole current production caller clamps the limit with .max(1), so this
isn't reachable from the HTTP surface today — but it's a latent contract
violation in a public Database API that any future caller can trip.

Fix

Guard limit == 0 at the top of get_channel_window and return an empty,
exhausted page (rows: [], has_more: false, next_cursor: None). This
honors the invariant and also skips a pointless database round-trip for a
zero-row request.

Test

Adds channel_window_zero_limit_reports_empty_exhausted_page, mirroring the
existing channel_window_* Postgres tests: it inserts rows, requests a
zero-limit window, and asserts the page is empty, has_more is false, and
next_cursor is None. (Marked #[ignore = "requires Postgres"] like its
siblings.)

Verification

  • cargo fmt -p buzz-db -- --check — clean
  • cargo check -p buzz-db --tests — clean
  • cargo clippy -p buzz-db --tests — clean

The new test requires Postgres and follows the repo's existing
#[ignore = "requires Postgres"] convention.


🤖 Generated with Claude Code

@hasky00
hasky00 requested a review from a team as a code owner July 29, 2026 13:58
@hasky00
hasky00 force-pushed the fix/channel-window-zero-limit branch from 13c6811 to b0ec12a Compare July 29, 2026 14:01
… is 0

get_channel_window derived `has_more` from the pre-truncation `limit + 1`
probe but read `next_cursor` from the truncated row set, so a `limit == 0`
call returned `has_more = true` with `next_cursor = None` — breaking the
documented `Some` iff `has_more` invariant and panicking or infinitely
looping any caller that trusts it. Guard `limit == 0` and return an empty,
exhausted page, which also skips a needless database round-trip.

Adds a Postgres-gated regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Hasky <239224029+hasky00@users.noreply.github.com>
@hasky00
hasky00 force-pushed the fix/channel-window-zero-limit branch from b0ec12a to 031424a Compare July 29, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant