Skip to content

perf(meta): lower a repo list filter once per query on PostgreSQL - #785

Merged
ThomasK33 merged 3 commits into
mainfrom
fix-pg-list-filter-lower-once
Nov 9, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
fix-pg-list-filter-lower-once

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Why

On PostgreSQL, GET /repos?q= and ?prefix= cost about rows × filter length. url ILIKE p lowers the pattern p again for every row it tests. In a multibyte database, Generic_Text_IC_like (like.c in PostgreSQL 17 and 18) calls lower() on the pattern and on the url, and then matches the two as LIKE does. So one org:read request with a long q or prefix can hold a PostgreSQL backend for seconds or minutes (numbers below). SQLite and S3-WAL already lower the filter once per query (#783).

What changes

  • PostgreSQL's ListRepos filters with lower(url) LIKE (SELECT lower($2::text)) instead of url ILIKE $2. That is the match ILIKE computes for each row, so every answer stays the same. The scalar subquery is an InitPlan: PostgreSQL lowers the pattern once per query, in custom and generic plans alike. The per-row match is bounded by the url's length, not by the filter's.
  • The SQL moves into pgListReposQuery, so a test can plan the statement that ListRepos runs.
  • No new limit, no new error and no API change.
  • internal/meta/README.md states the invariant. The SQLite comment that cited url ILIKE ? names the new form.

Why not answer an empty page above a url length bound

  • No write path bounds a url. Only CreateRepo and CreateRepoWithState write url (PostgreSQL, SQLite and S3-WAL), and the API takes it from the token's repo claim without a length check.
  • PostgreSQL's (org_id, url) btree index limits the compressed index entry, not the url. The test PostgreSQL stored a 100,005-byte url of one repeated letter and refused a 4,005-byte varied one. So no length bounds every url, and a shortcut above some length would change answers.

Verification

  • TestPGListReposLowersFilterOnce plans the ListRepos statement with EXPLAIN (GENERIC_PLAN), the kind of plan that pgx's statement cache can run. It requires an InitPlan, no read of the pattern parameter anywhere in the plan, and a filter of lower(url) ~~ (LIKE) with no ~~* (ILIKE). ILIKE lowers its pattern again for each row even when an InitPlan computes that pattern. On main's url ILIKE $2 it fails: the scan's filter reads $2 for every row.
  • TestPGListReposMatchesILIKE runs 592 filters over 23 urls and compares each page with url ILIKE on the same rows: the same urls in the same order. The urls fold outside ASCII (ß and ẞ, İ and ı, Σ σ ς, Dž, ffi, the Kelvin and Ångström signs, a combining accent) and hold %, _ and \. The filters are each url rune in every case Go knows for it, alone and after acme/, and each start of each url.
  • fix: list repos for a q or prefix of any length, and refuse NUL and invalid UTF-8 #783's TestListReposLongFilters (a q and a prefix of 200 KiB list nothing, and 30,000 percent signs match only their url) passes unchanged on PostgreSQL, SQLite and in-memory S3-WAL.
  • Mutants at 26d6040, each fails a test (7 of 7): main's url ILIKE $2, lower(url) LIKE lower($2::text) without the subquery, url ILIKE (SELECT lower($2::text)) and lower(url) ILIKE (SELECT lower($2::text)) fail only TestPGListReposLowersFilterOnce. A url that is not lowered fails it, TestPGListReposMatchesILIKE and the existing ListRepos tests. A pattern that is not lowered and ESCAPE '#' fail TestPGListReposMatchesILIKE and the existing ListRepos tests.
  • Probe (not committed), at f4ddb85 (26d6040 changes only the plan test): 3,100 repos in one org, PostgreSQL 17.11, UTF-8 with en_US.utf8, median of 3 runs. The host load average (1 min) read 6.10 at the start and 93.94 at the end.
Filter main (ILIKE) This PR
q of 49,990 bytes 3.38 s 1.7 ms
q of 200 KiB 16.6 s 3.7 ms
q of 1 MiB 92.1 s 27.0 ms
prefix of 200 KiB 19.8 s 6.0 ms
prefix of 1 MiB 84.7 s 20.3 ms
  • At 26d6040, with -race: build, vet (with and without -tags=openstorage_viewcheck), golangci-lint (0 issues), the doc-link check, the script unit tests and both --fingerprints-only checks pass. internal/meta and internal/api pass with -tags=openstorage_viewcheck on PostgreSQL and SQLite.

Size

205 changed lines: 51 of production Go, 6 of a SQLite comment, 147 of tests and 1 of docs.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

Change-Id: I5e7433bf15297431dc114deb5bfa612fbe2034cc
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I97e7a315892d46babb307f4727baf7529285f881
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T15:08:28.597200Z 26d6040 Manual request
🔒 Security Review ✅ Completed 2026-11-09T15:09:28.835988Z 26d6040 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: f4ddb854d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: f4ddb854d2

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Change-Id: I2f2387113dadc6e2dfd9138ea7840b91dc232217
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 26d6040b57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 26d6040b57

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 3ceb600 Nov 9, 2026
10 of 11 checks passed
@ThomasK33
ThomasK33 deleted the fix-pg-list-filter-lower-once branch November 9, 2026 15:54
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