boostorg / boostorg/website-v2
[BUG (Pre-existing)] [Backend] Boost versions are ordered and compared as strings, which breaks at 1.100.0
- Dominant language
- HTML
- Stars
- 18
- Forks
- 28
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 77
Description
## Goal
Replace every lexicographic comparison of Boost version names with a numeric
comparison, so that version ordering, "most recent version" lookups, and every
`min_version` / `max_version` range check stay correct once a minor number reaches
three digits.
---
## Context
Boost version identifiers are compared as plain strings throughout the codebase.
That happens to agree with numeric order for every version released so far, and
stops agreeing the moment a minor number grows a digit:
```python
'boost-1.100.0' >= 'boost-1.85.0' # False
'boost_1_100_0' >= 'boost_1_85_0' # False
```
Latest released version is 1.92.0, and the database currently holds 95 versions.
Verified against that data: lexicographic order and numeric order agree at every
position today, and there are no single-digit minor numbers left to trip it
early. So nothing is broken right now - this is a dormant fault that fires on the
release of 1.100.0 and silently mis-orders everything after it.
The codebase already contains the correct machinery.
`VersionQuerySet.with_version_split()` annotates `version_array` as a real
Postgres `int[]` built from the name, plus `major` / `minor` / `patch`, and it
orders correctly. One management command already uses it
(`import_library_version_website_adoc` filters on `version_array__gte`). The work
is to route the remaining comparisons through the same annotation rather than to
invent anything.
### Why this matters beyond ordering a dropdown
The most consequential caller is the release pipeline. `ReleaseTasksManager`
takes the newest version and hands its *name* to the commit importer as a floor:
```python
self.latest_version = Version.objects.with_partials().most_recent()
...
self.handled_commits = update_commits(min_version=self.latest_version.name)
```
Both halves of that are string-based. `most_recent()` is
`order_by("-name").first()`, so after 1.100.0 ships it returns the wrong version,
and the floor derived from it then selects the wrong set of versions to walk. The
importer imports the wrong range and reports success.
---
## Scope
Backend only. No template, CSS, or model-field changes; `version_array` is an
annotation, so no migration is required.
Two things are in scope and should be treated as one job, because fixing either
alone leaves the pipeline wrong: the **origin** of a version comparison value,
and the **comparisons** made with it.
---
## Acceptance Criteria
### 1. "Most recent version" is numeric
- `VersionQuerySet.most_recent()` and `most_recent_beta()` no longer order by
`-name`. Both order by `version_array` descending, via `with_version_split()`.
- `with_version_split()` drops anything not matching
`^(boost-)?\d+\.\d+\.\d+$` from the queryset, so `master`, `develop` and beta
names are excluded by it. Confirm each converted caller still gets the rows it
expects, and keep the existing `beta` / `full_release` / `active` filters
intact.
- Any other `order_by("name")` or `order_by("-name")` on `Version` that is meant
to express version order, rather than alphabetical order, is converted too.
Audit the whole repo for this; the dropdown ordering path is the likely second
case.
### 2. The commit importer compares numerically
Three sites in `libraries/github.py`:
- `get_commit_data_for_repo_versions` skips version pairs with
`if a < min_version and b < min_version`, on names. The version list it walks
is already correctly ordered by `version_array`; only the floor test is
lexicographic.
- `LibraryUpdater.update_commits` builds its `library_versions` map with
`version__name__gte=min_version`.
- The same method scopes its destructive delete with
`library_version__version__name__gte=min_version`. This one deliberately
mirrors the line above it so that the delete and the rebuild always choose the
same versions - the two must keep matching exactly after the change, or a clean
import will delete more than it rebuilds.
Preferred shape: accept the floor as a parsed `[major, minor, patch]` list (or
convert it once on entry) and filter on `version_array__gte`, the way
`import_library_version_website_adoc` already does.
### 3. Range checks in `libraries/utils.py` compare numerically
`version_within_range(version, min_version, max_version)` is documented as
"Direct string comparison". It has five callers across `libraries/tasks.py` and
`versions/tasks.py`, and it is used against **two different name formats**:
| Consumer | Values compared | Format |
|---|---|---|
| `LIBRARY_DOCS_EXCEPTIONS` | `version.boost_url_slug` | `boost_1_29_0` |
| `LIBRARY_DOCS_MISSING` | `version.name` | `boost-1.34.0` |
| `MAXIMUM_BOOST_DOCS_VERSION` (`"boost-1.30.2"`) | `version.name` | `boost-1.30.2` |
Each pairing is internally consistent today and each is lexicographic, so all
three break at 1.100.0.
- The helper parses both formats into numeric parts and compares those. It must
keep accepting the underscore form, since `LIBRARY_DOCS_EXCEPTIONS` holds
around a dozen entries in that format and rewriting them is a larger, riskier
change than teaching the comparison to read them.
- A value the helper cannot parse must fail loudly rather than silently falling
back to a string compare. A silent fallback is what makes this class of bug
invisible.
- Patch numbers matter here: `MAXIMUM_BOOST_DOCS_VERSION` is `boost-1.30.2`, so
the comparison cannot be reduced to major/minor.
### 4. The docs-URL import command compares numerically
`import_library_version_docs_urls` filters with
`.filter(name__gte=f"boost-{min_version}")`. Convert it to `version_array__gte`,
matching its sibling command `import_library_version_website_adoc`, which is
already correct and is the reference implementation for this ticket.
### 5. Tests
- A test asserting `most_recent()` picks `1.100.0` over `1.99.0`. This is the
regression that would take the release pipeline down, and it cannot be written
against current data - it needs a fixture with a three-digit minor.
- A test per converted comparison site covering the three-digit case, including
one for each name format the range helper accepts.
- A test that the commit importer's rebuild scope and its destructive-delete
scope select the same versions for a given floor, so they cannot drift apart
again.
- A test that an unparseable version raises rather than silently comparing as a
string.
---
## Out of Scope
- Adding a stored, indexed version-order column. `version_array` is computed per
query, which is fine at 95 rows; if profiling later says otherwise that is its
own ticket.
- Rewriting the `LIBRARY_DOCS_EXCEPTIONS` constants into a single name format.
- Changing what the release pipeline does with the floor once it has the right
one.
---
## Risks
- `with_version_split()` filters out non-numeric names. Converting a caller that
currently relies on `master` or `develop` appearing in its queryset will
silently shorten that queryset. Check each converted call site for this rather
than assuming.
- The docs-URL exception constants drive which URL is generated for old library
docs. A wrong comparison there produces broken documentation links for early
versions rather than an error, so those conversions want their own before/after
spot check across a few old library-versions.
- The commit importer's delete and rebuild must be converted together. Leaving
one on names and one on numbers reintroduces a wider-than-rebuild delete, which
destroys commit rows that are not re-created.
---
## Testing Steps
1. Insert a `Version` named `boost-1.100.0` alongside the existing versions.
2. `Version.objects.with_partials().most_recent()` must return it, not
`boost-1.99.0` or `boost-1.92.0`.
3. Run the version dropdown and confirm 1.100.0 sorts above 1.92.0.
4. Run `import_library_version_docs_urls` with a floor of `1.100.0` and confirm
it selects only that version, not everything from 1.10 upward.
5. Check `version_within_range('boost-1.100.0', min_version='boost-1.85.0')` is
`True`, and the same for the underscore format.
6. Confirm a library-version whose docs are governed by a
`LIBRARY_DOCS_EXCEPTIONS` range still resolves to the same docs URL as before
the change, for a version well inside the range and one just outside it.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with VersionQuerySet.with_version_split() and the existing import_library_version_website_adoc command, then audit version ordering and floor comparisons in libraries/github.py, libraries/utils.py, libraries/tasks.py, and versions/tasks.py. Add regression coverage for 1.100.0, both accepted name formats, matching importer scopes, and parse failures; done means all numeric comparisons and tests pass without changing nonnumeric-name behavior unexpectedly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, python
- Domain
- backend, databases, release
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 56/100