Skip to content

fix: read the real namespace and extension ids out of a lateral join - #2120

Merged
netomi merged 3 commits into
mainfrom
fix/jooq-derived-table-id-mapping
Sep 1, 2026
Merged

netomi merged 3 commits into
mainfrom
fix/jooq-derived-table-id-mapping

Conversation

@netomi

@netomi netomi commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Found while investigating a staging deployment where search returned nothing and "My Extensions" was empty. Both symptoms have this one cause, and it is not new — the defect dates from 2024, but only became visible when something started comparing the ids.

The bug

ExtensionVersionJooqRepository.TableFieldMapper remaps static field constants onto the CROSS JOIN LATERAL derived table that carries an extension's latest version, so callers can read the row with the familiar EXTENSION_VERSION.X constants. It assumed Table.field(Field) answers null for a field belonging to another table — its javadoc said so explicitly, "they have no lineage relationship".

jOOQ resolves by name, not by lineage. The lateral selects EXTENSION_VERSION.ID, so it has a column called id, and both NAMESPACE.ID and EXTENSION.ID were answered with it:

persisted   namespace=5  extension=9  version=2
returned    namespace=2  extension=2  version=2      ← all the version's id

Names and every other column map correctly. Only same-named columns collide, the value has the right type, and nothing throws — callers just read a version id where they asked for a namespace or extension id.

Why two pages went empty

  • My Extensions — UserAPI:263 restricts a user's extensions to the namespaces they are a member of, comparing namespace ids. No version id is ever a member's namespace id, so the list is always empty. That filter arrived in change: make extension versions immutable #1983 (v1.1.0), which is what turned a latent fault into a visible one.
  • Search — getLatestVersions(result) keys its results by extension id, so every hit looks absent from the database. LocalRegistryService:970 then deletes such hits from the search index, so each search permanently removed the entries it could not resolve. A rebuild is needed after deploying this; the fix stops the bleeding but does not restore what was purged.

The fix

Remap only fields that actually belong to the lateral's source table:

if (field instanceof TableField<?, ?> tableField
        && tableField.getTable() == EXTENSION_VERSION
        && tableField.getTable() != table) {

The test, and a warning about it

ExtensionVersionJooqRepositoryIdMappingTest covers both affected queries — findLatest(UserData) and findLatest(Collection<Long>) — asserting the namespace and extension ids are the persisted ones.

It pads each sequence by a different amount and asserts the three ids are distinct before testing anything. That guard is the point: my first version of this test passed with the bug present, because in a fresh database the namespace, extension and version all landed on id 2 and the assertions could not fail either way. Anyone editing this test should keep the ids apart.

Fail-first checked: reverting the mapper turns both cases red with expected: 9L.

Full suite green at 1126 tests.

Follow-up worth considering separately

The purge at LocalRegistryService:967-971 removes index entries whenever the database lookup comes back short, with nothing logged as unusual — so a code defect in that lookup silently destroyed data rather than degrading the display. Removing an entry only when the extension is genuinely absent would have made this incident a rendering bug instead.

🤖 Generated with Claude Code

TableFieldMapper remaps static field constants onto the CROSS JOIN LATERAL
derived table that carries an extension's latest version. It assumed
Table.field(Field) answers null for a field of another table - its javadoc
said so - but jOOQ resolves by name, not by lineage. The lateral selects
EXTENSION_VERSION.ID, so it has a column called "id", and both NAMESPACE.ID
and EXTENSION.ID were answered with it. The value has the right type and
only its meaning is wrong, so nothing failed: callers simply read a version
id where they asked for a namespace or extension id.

Two pages went empty as a result. UserAPI restricts a user's own extensions
to the namespaces they are a member of, comparing namespace ids, and no
version id is ever a member's namespace id. Search keys its results by
extension id, so every hit looked absent from the database - and
LocalRegistryService then deletes such hits from the search index, so each
search permanently removed the entries it could not resolve.

Only fields of the lateral's own source table are remapped now.

The test forces the namespace, extension and version ids apart before
asserting anything. Written the obvious way they all land on the same
sequence number in a fresh database, and the assertions hold whether the
mapping is right or wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes incorrect ID mapping when reading latest extension versions via CROSS JOIN LATERAL by preventing jOOQ’s name-based field resolution from remapping NAMESPACE.ID / EXTENSION.ID onto the derived table’s id column. Adds a regression test to ensure namespace/extension/version IDs are read correctly for the affected query paths.

Changes:

  • Restrict lateral-table field remapping to fields that actually belong to Tables.EXTENSION_VERSION.
  • Add ExtensionVersionJooqRepositoryIdMappingTest covering both findLatest(UserData) and findLatest(Collection<Long>).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java Tightens TableFieldMapper remapping to avoid ID collisions from name-based resolution.
server/src/test/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepositoryIdMappingTest.java New regression test asserting namespace/extension IDs are not overwritten by the version ID.
Suppressed comments (1)

server/src/test/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepositoryIdMappingTest.java:101

  • cleanUp(ids) is executed only on the success path; if an assertion fails, the shared Postgres container will keep the inserted rows and can make subsequent tests fail (e.g., via unique constraints on names). Wrap the assertions in a try/finally so cleanup runs even when the test is red.

        cleanUp(ids);
    }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

netomi and others added 2 commits September 1, 2026 22:14
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Thomas Neidhart <thomas.neidhart@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Thomas Neidhart <thomas.neidhart@gmail.com>
@netomi
netomi merged commit 17e59a6 into main Sep 1, 2026
5 checks passed
@netomi
netomi deleted the fix/jooq-derived-table-id-mapping branch September 1, 2026 20:35
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.

2 participants