Skip to content

Count the rows a request matches - #8

Merged
azer merged 2 commits into
mainfrom
count
Oct 8, 2026
Merged

azer merged 2 commits into
mainfrom
count

Conversation

@azer

@azer azer commented Oct 8, 2026

Copy link
Copy Markdown
Member

API clients need the size of a listing without paging through it, e.g. how many favorites a user has. This adds the one reserved key and the one function bind needs for that.

What changes

  • count=true (or count=1) is a reserved key like sort, limit and start. Bind.count?/1 reads it from a params map or a query string.
  • Bind.count/1 takes the query Bind.query/3 built, with any scopes the caller piped after it, and returns a query that selects how many rows it matches. Filters, joins and the cursor stay; sort, limit and preloads drop. Run it with Repo.one/1.
  • The count wraps the rows in a subquery, so a scope's distinct, group_by or select never changes the number.
  • README gets a Counting section. Version 0.14.0.
query = Bind.query(params, User) |> scope_to_viewer(viewer)
users = Repo.all(query)
count = if Bind.count?(params), do: Repo.one(Bind.count(query))

Tests

test/count_test.exs covers the flag in both param shapes, filters kept, sort and limit dropped (including the implicit default limit of 10), cursor kept, scopes and preloads piped after query/3, and the subquery form around a distinct.

Consumers: mitteai/mitte.ai (list endpoints answer ?count=true) and the fs.list tool in socket-service, in follow-up PRs.

🤖 Generated with Claude Code

`count=true` is a reserved key next to sort, limit and start. Bind.count/1
takes the query Bind.query/3 built, with the caller's scopes piped after it,
and selects how many rows it matches: filters, joins and cursor stay, sort,
limit and preloads drop. It counts a subquery, so a scope's distinct,
group_by or select never changes the number. Version 0.14.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The additions are small, correctly integrated with existing param/query handling, and covered by focused tests, with no objective issues found.

0 open findings

What changed in this PR

This PR adds row-counting support to Bind, an Elixir library that builds Ecto queries from request params. API clients can now learn how many rows a listing matches without paging through them. It introduces one reserved param key (count) and two public functions, plus documentation and a version bump.

Changes:

  • Adds Bind.count?/1 to detect count=true/count=1 from a params map or query string.
  • Adds Bind.count/1, which transforms a Bind.query/3 result (plus any piped scopes) into a count query by dropping order_by/limit/offset/preload and wrapping the remainder in a subquery that selects count(), keeping filters/joins/cursor intact.
  • Documents the feature (README "Counting" section, fixes the Bind.query(params, schema) signature) and bumps the version to 0.14.0.
File Description
lib/​bind.ex Implements count?/1 and count/1 with the subquery-based counting logic.
test/​count_test.exs New tests covering flag detection, retained filters/cursor/scopes, dropped sort/limit/preloads, and the distinct subquery case.
README.md Adds a "Counting" section and corrects the Bind.query(params, schema) API signature.
mix.exs Bumps version from 0.13.0 to 0.14.0.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread lib/bind.ex
query
|> exclude(:order_by)
|> exclude(:limit)
|> exclude(:offset)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count keeps the pagination cursor, so a paginating client gets a shrinking total.
start is added as a WHERE on id, so exclude(:offset) drops nothing here.
On page 2+ the number is only the rows after the cursor.
If the goal is a listing size, count the filters without the cursor.

@hanafrommitte hanafrommitte left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count query itself is correct, but it keeps the pagination cursor.
start is a WHERE on id, not an offset, so excluding :offset drops nothing.
A client that pages and asks for count gets a partial total on page 2+.
Drop the cursor for a listing size, or say the number shrinks as you page.

1 suggestion inline — none are blocking.

…om page one

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@azer

azer commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Kept the cursor and said so: the @doc and README now state that with start/-start the count is the rows past the cursor and shrinks as a client pages, and that the total comes from the first page. Dropping the cursor would need every controller to rebuild its scoped query a second time; the first page is where clients ask (mitteai/mitte.ai#626's resource.count never sends one).

@azer
azer merged commit 39a522c into main Oct 8, 2026
3 checks passed
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.

3 participants