Skip to content

fix(projects): paginate and filter the project list server-side - #57

Merged
tanmaykm merged 2 commits into
mainfrom
kr/project-list-paginate
Sep 15, 2026
Merged

tanmaykm merged 2 commits into
mainfrom
kr/project-list-paginate

Conversation

@krynju

@krynju krynju commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

jh project list sent the Projects GraphQL query with only ownerId as a variable — no limit, offset or filter — and filtered by owner client-side. Hasura therefore computed every project on the instance (57,508 on nightly.juliahub.dev).

That query is very expensive per row for an admin caller: the resources field re-materialises the hasura.resources view (UNION over all projects with a volatile plpgsql function) and the access_control_resources view for every project row — roughly 1 s/row. The request always hit the CLI's 30 s http.Client timeout (nginx 499), but Hasura does not cancel the Postgres statement when the client disconnects, and nightly has statement_timeout = 0. Every nightly TestProjectList + TestProjectListByUser run left two statements each pinning a vCPU for hours (observed: two backends at 5h+ running exactly this SQL, started 30 s apart, matching the two 30.0 s 499s from Go-http-client/2.0 as github-ci@juliahub.com).

Fix

  • Paginate: fetch in pages of 100 with limit/offset, order_by: created_at desc. New --limit flag caps the total (default 100, 0 = all); output says when it is showing a subset.
  • Filter server-side: --user → owner_id: {_eq: <me>}; --user <name> → owner: {username: {_ilike: <escaped name>}} (same case-insensitive exact-match semantics as the old EqualFold, LIKE metacharacters escaped).
  • Drop resources, users, groups from the query. resources only ever yields the constant Files/Datasets pseudo-resources for every project (it's a view over projects), and users/groups were fetched but never printed.
  • Reuse the shared executeGraphQL helper instead of a duplicated HTTP client.
  • Unit tests for buildProjectsFilter / escapeLikePattern; CLAUDE.md updated.

Measurements

Nightly-shaped local instance (pg16 + JuliaHub full_schema.sql + Hasura v2.48.16 with the platform's metadata; 57.5k projects, 146 users, caller in the admin group), end-to-end /v1/graphql:

query limit 100
before (old shape, resources included) 87.3 s
after 0.23 s

Unbounded before-shape over all 57.5k rows extrapolates to ~17 h, which is what nightly's Postgres was doing.

make check (fmt, vet incl. e2e tag, unit tests, e2e compile) passes. Output format is unchanged so e2e/project_test.go's Found N project(s) matcher still holds.

Server-side follow-ups (JuliaHub repo, separate): a statement_timeout for the Hasura DB role so abandoned queries die, and restructuring hasura.resources / access_control_resources so the resources field stops costing ~1 s/row.

🤖 Generated with Claude Code

`jh project list` sent the Projects GraphQL query with only `ownerId` set:
no limit, offset or filter. Hasura therefore computed every project on the
instance (57k on nightly.juliahub.dev) and the CLI filtered by owner
client-side. The query costs the server roughly a second per row for an
admin user because the `resources` field re-materialises the
hasura.resources / access_control_resources views for every project, so the
request always hit the 30 s client timeout — and Hasura does not cancel the
Postgres statement when the client disconnects. Each nightly e2e run left
two statements pinning a vCPU each for many hours.

- fetch in pages of 100 with limit/offset and order_by created_at desc;
  --limit caps the total (default 100, 0 = all)
- filter by owner in the GraphQL where clause (owner_id for --user,
  owner.username _ilike for --user <name>) instead of client-side
- drop the resources / users / groups fields: resources only ever yields the
  constant Files/Datasets pseudo-resources and users/groups were never
  printed
- reuse executeGraphQL instead of a hand-rolled HTTP client

Measured on a nightly-shaped local instance (57.5k projects, admin caller,
limit 100): 87.3 s -> 0.23 s.

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

Copy link
Copy Markdown

hey @pfitzseb and @thelonewolf1603 can you please take a look at this PR?

@thelonewolf1603

Copy link
Copy Markdown
Contributor

@claude review

Comment thread main.go Outdated
Review feedback: expose pagination as `--page=N` rather than a `--limit`
that the CLI satisfies by looping over pages internally.

`jh project list [--page N]` now issues exactly one GraphQL request per
invocation: `limit: 100, offset: (N-1)*100`. The header still prints the
server-side total and, when there is more than one page, which page is
being shown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@krynju
krynju requested a review from pfitzseb September 9, 2026 14:28

@pfitzseb pfitzseb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM but would be good to get a review from @thelonewolf1603 too.

@tanmaykm
tanmaykm merged commit 92868d4 into main Sep 15, 2026
1 check passed
@tanmaykm
tanmaykm deleted the kr/project-list-paginate branch September 15, 2026 03:25
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.

5 participants