Rework prerelease limit SQL - #2106
Conversation
gnugomez
left a comment
There was a problem hiding this comment.
looks good to me, although I'm not that far into jooq, I had to search for a lot of things you're doing here 😅
There was a problem hiding this comment.
Pull request overview
This PR reworks the SQL used to cap pre-release versions per extension by switching from a correlated subquery approach to a window-function-based ranking, then filtering by rank.
Changes:
- Introduces a
dense_rank()window function and (optionally) wraps the base query in a CTE to filter pre-releases by rank. - Switches the “minimal” row mapping for this query to use explicit column aliases plus a new
RankedFieldMapper. - Refactors
FieldMapperimplementations (including renaming the default mapper toIdentityFieldMapper) to support the new query shape.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Code review🔴 Confirmed bug: SQL error on the default (uncapped) config
.and(field(name("extension_version_target_platform")).eq(targetPlatform))unconditionally, but that alias only exists in the Reproduced directly against a real Postgres instance: Since VS Code's 🟡 Plausible bug:
|
findAllActiveByExtensionIdAndTargetPlatform appended the target
platform condition via field(name("extension_version_target_platform"))
unconditionally, referencing a select-list alias from the WHERE clause
of the same query. That is only valid once the query has been wrapped
in the ranked_extension_version CTE (maxPreReleaseVersions >= 0); with
the cap disabled (the default, -1) there is no CTE, and Postgres
rejects the query outright once a target platform is supplied - which
VS Code's extensionQuery always does. Now the capped branch keeps
using the typed CTE field lookup, and the uncapped branch applies the
condition directly against the real EXTENSION_VERSION.TARGET_PLATFORM
column of its own (non-wrapped) query.
Also added nullsLast() to the semver ordering the "rank" window
function uses: Postgres defaults DESC to NULLS FIRST, so a legacy
version row with no parsed semver would otherwise outrank every real
semver version and could push the true latest pre-release out of the
capped result set, contradicting this method's own "rank #1 is never
dropped" guarantee.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ranking Ports the pre-rework findAllActiveByExtensionIdAndTargetPlatform (a correlated COUNT(*) subquery per pre-release row, O(k^2) for an extension with k pre-releases) as a test-local helper, purely so it has something to compare the reworked denseRank() window-function query against - the original method itself is gone. Measured against a real Postgres instance with 3000 pre-release versions on one extension (the volume this method's own doc comment is written around): old ~3023ms, new ~56ms, roughly a 54x speedup. The assertion allows a generous margin (3x + 500ms) rather than pinning an exact ratio, since this is real wall-clock timing against a container DB, not a controlled microbenchmark - the point is to catch a severe regression, not chase a precise number. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up: regression test + performance test for the fixes abovePushed two more commits addressing the issues from the review:
|
…pping TableFieldMapper.map(...) only called table.field(field) - the actual remap onto the derived table - when the field's owning table already equaled table, which never happens for a static-table field constant (EXTENSION_VERSION.ID's table is always the static EXTENSION_VERSION table, never a derived "latest" table). So it always returned the field unchanged, regardless of intent: dead code, masked only because Record.get(...) happens to resolve those raw fields against the correct row column anyway via its own field-matching fallback. Flipped the condition so it actually remaps whenever the field belongs to a different table (the case that matters), falling back to the unchanged field when table.field(field) itself returns null - which happens for a field with no lineage relationship to table at all, e.g. NAMESPACE.ID/EXTENSION.ID read through the same mapper instance alongside genuine EXTENSION_VERSION.* fields in toExtensionVersionCommon. Confirmed the fix doesn't regress: it compiled and passed 1064/1064 tests, including a new regression test that would fail if the wrong same-named "id" column were ever read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Coolio |
Extends the performance test with a third baseline: a port of findAllActiveByExtensionIdAndTargetPlatform as it existed before eclipse-openvsx#2062 introduced pre-release limiting at all - a bare filtered SELECT with no ranking whatsoever. The reworked query computes its denseRank() window function unconditionally, even on the uncapped (default, max-pre-release-versions=-1) path where nothing reads it, so this checks that overhead isn't a meaningful regression against the simplest query that could possibly answer the same question - the path every caller takes unless an operator opts into the cap. Measured (3000 pre-release versions, same run): - old capped (correlated subquery): 1620ms - new capped (window function): 20ms - no-limit-at-all baseline (pre-eclipse-openvsx#2062): 9ms - new uncapped (unused window function): 132ms The capped comparison remains a clear win (~80x here). The uncapped comparison shows the "computed even when unused" window function is a real, measurable overhead (~14x the baseline in this run) - still comfortably fast in absolute terms and within the assertion's generous margin, but a genuine cost worth having on record rather than only asserted away. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up: extended the performance test with a third baselinePushed Reasoning: the reworked query computes its Measured (3000 pre-release versions, same run, same margin-based assertions as before — not a tight ratio):
So: the capped path is a clear, large win, as expected. The uncapped path shows a real, measurable cost from computing the window function even when it's discarded — this matches the efficiency note from the earlier review ( Full suite: 1064/1064 passing. 🤖 Generated with Claude Code |
Two changes to close the ~14x gap the extended performance test found between the uncapped path and the pre-limiting baseline: 1. Skip building the denseRank() window function entirely when the cap is disabled (the default), instead of computing it and simply not reading the result. This also let the uncapped path drop the aliasing/CTE machinery altogether: it now selects the same plain, unaliased columns the pre-eclipse-openvsx#2062 method did and maps them via the existing toExtensionVersion/IdentityFieldMapper path, rather than sharing one aliased field list with the capped path and needing RankedFieldMapper to translate between them. The aliasing exists only because the capped path's CTE needs every column uniquely named to reference by string (NAMESPACE.ID/EXTENSION.ID/ EXTENSION_VERSION.ID would otherwise all render as "id") - the uncapped path never had that requirement. 2. This isolated the real dominant cost, which turned out to be RankedFieldMapper itself (still needed for the capped path): it resolved each mapped column by constructing a brand-new, unrelated Field object and handing that to row.get(...), which forces jOOQ into a slow fallback lookup. Resolving through the row's own name-indexed field(String) lookup instead - the row's native, fast path - fixed it. (The derived name string, a pure function of the static field, is still cached; only the per-row Field reconstruction was the problem.) Measured (3000 pre-release versions, same run): before: no-limit baseline ~19ms, new uncapped ~127ms (~7x) after denseRank() skip: no-limit baseline ~19ms, new uncapped ~132ms (no change - not the bottleneck) after RankedFieldMapper fix: no-limit baseline ~19ms, new uncapped ~25-47ms after the field-list split: no-limit baseline ~32ms, new uncapped ~39ms toExtensionVersion (used by the uncapped path) is now package-private so the performance test can build a fair, equally-mapped baseline instead of a mapping-free one that would have understated the actual per-row cost both approaches pay. Full suite: 1064/1064 passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up: fixed the uncapped-path regressionPushed 1. Skipped 2. That alone didn't move the needle — measured no change (still ~130ms vs ~19ms baseline). Dug further and found the actual dominant cost: Once both were in, it became clear the uncapped path didn't need the aliasing/CTE machinery (or Progression (3000 pre-release versions, same dataset each time):
Also made Full suite: 1064/1064 passing. 🤖 Generated with Claude Code |
netomi
left a comment
There was a problem hiding this comment.
I think this PR is now ready for merging.
The old uncapped version is restored to not have any regression, the new capped version is now comparable in terms to speed and added some tests to compare the new version with the baseline.
Optimize the ranking SQL