Skip to content

fix: make API list ordering deterministic - #1642

Open
MiquelRForgeFlow wants to merge 1 commit into
PokeAPI:masterfrom
MiquelRForgeFlow:fix/deterministic-query-ordering
Open

fix: make API list ordering deterministic#1642
MiquelRForgeFlow wants to merge 1 commit into
PokeAPI:masterfrom
MiquelRForgeFlow:fix/deterministic-query-ordering

Conversation

@MiquelRForgeFlow

@MiquelRForgeFlow MiquelRForgeFlow commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Most ORM queries had no explicit ORDER BY, so the database was free to return rows in any order. The same dataset could then be serialized differently on every build, turning each api-data regeneration into a huge diff of pure reordering noise.

Give PokeApiModel a default manager that orders by primary key. Managers are inherited from abstract bases and reverse relation managers derive from the model's default manager, so this covers plain queries, reverse relations and prefetches at once, including the declarative reverse fields (Generation, GrowthRate, PokemonColor, PokemonShape, PokemonHabitat) that no serializer call site could order.

Add tie-breakers to the partial orderings that left equal keys undefined, ending every order_by() with the primary key: pokemon moves and held items, location area and pokemon encounters, past abilities, stats and types, form and pokemon types, berry flavors, pokedex entries and encounter method rates. The few calls left untouched already order by order_by("id"), which is the primary key.

Document in the manager that a distinct(*fields) call has to spell out a matching order_by(), since PostgreSQL requires the leading ORDER BY expressions to match the DISTINCT ON ones. A plain distinct() is fine, because the primary key is already part of the selected row.

Add two regression tests that enforce the ordering, one for a declarative reverse relation and one for rows tying on the whole ORDER BY. Both insert rows with primary keys out of order, so both fail without this fix.

No migration needed; call sites needing another order still override it.

AI coding assistance disclosure

Of course, the PokeApiManager was AI idea. Also used to check nothing is left out.

Contributor check list

  • I have written a description of the contribution and explained its motivation.
  • I have written tests for my code changes (if applicable).
  • I have read and understood the AI Assisted Contribution guidelines.
  • I will own this change in production, and I am prepared to fix any bugs caused by my code change.

@Naramsim

Copy link
Copy Markdown
Member

Hi @FallenDeity Can you chime in and review this PR? Maybe I'm wrong but your current re-implementation is already preserving a custom-order of the fields, am I wrong? On my PC when building the current master branch I always get the same results/order.

If this PR enforces the order or a specific sorting strategy we can merge it in!

@MiquelRForgeFlow Thanks for the PR!

@Naramsim Naramsim 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.

I trust the tests on this PR. I checked it out locally and everything seems to run

@phalt

phalt commented Aug 21, 2026

Copy link
Copy Markdown
Member

I remember trying to do this years ago. One of those ones where you see a solution and realise "damn, it was that easy?" Will make reviewing the pokeapi-data diffs in the future far easier 🙏

@FallenDeity

FallenDeity commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Hi @FallenDeity Can you chime in and review this PR? Maybe I'm wrong but your current re-implementation is already preserving a custom-order of the fields, am I wrong? On my PC when building the current master branch I always get the same results/order.

If this PR enforces the order or a specific sorting strategy we can merge it in!

@MiquelRForgeFlow Thanks for the PR!

i did not do anything for order in my prior pr it was mainly just serializer query optimization, I have a few thoughts on why the same order is maintained few of the serializers already have order_by where now pk has been added as a fallback and also we freshly load the data in sqlite/postgres before serving which might tend to return the rows in same physical order its defined in the csv since there is no db reorg happening

this change is limited to the nested json lists and makes the behaviour consistent

like this for example

abilities = AbilitySummarySerializer(many=True, read_only=True, source="ability")

now it will call the query set on the manager with the ditto change i am working on it should make diff viewing a lot easier yeah

awesome work :)

few reviews

  • we should add a test case for this which enforces the order
  • wherever order_by is present pk should be as fallback its there in most places, few places no pk any reason for that
- BerryFlavorMap.objects.filter(berry_flavor=obj, potency__gt=0).select_related("berry").order_by("potency")
+ BerryFlavorMap.objects.filter(berry_flavor=obj, potency__gt=0)
            .select_related("berry")
            .order_by("potency", "berry_id")
        )

also add a comment in the manager that if distinct() is used the fields for both order_by() and distinct() must be same https://stackoverflow.com/questions/9795660/postgresql-distinct-on-with-different-order-by

Most ORM queries had no explicit ORDER BY, so the database was free to
return rows in any order. The same dataset could then be serialized
differently on every build, turning each api-data regeneration into a
huge diff of pure reordering noise.

Give PokeApiModel a default manager that orders by primary key. Managers
are inherited from abstract bases and reverse relation managers derive
from the model's default manager, so this covers plain queries, reverse
relations and prefetches at once, including the declarative reverse
fields (Generation, GrowthRate, PokemonColor, PokemonShape,
PokemonHabitat) that no serializer call site could order.

Add tie-breakers to the partial orderings that left equal keys undefined,
ending every order_by() with the primary key: pokemon moves and held
items, location area and pokemon encounters, past abilities, stats and
types, form and pokemon types, berry flavors, pokedex entries and
encounter method rates. The few calls left untouched already order by
order_by("id"), which is the primary key.

Document in the manager that a distinct(*fields) call has to spell out a
matching order_by(), since PostgreSQL requires the leading ORDER BY
expressions to match the DISTINCT ON ones. A plain distinct() is fine,
because the primary key is already part of the selected row.

Add two regression tests that enforce the ordering, one for a declarative
reverse relation and one for rows tying on the whole ORDER BY. Both insert
rows with primary keys out of order, so both fail without this fix.
@MiquelRForgeFlow
MiquelRForgeFlow force-pushed the fix/deterministic-query-ordering branch from 345e774 to e0e0362 Compare August 21, 2026 14:15
@MiquelRForgeFlow

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews! All three points addressed in the amended commit:

  • pk added to the seven order_by() calls missing it. The three left alone already order by the primary key via order_by("id").
  • distinct() caveat documented in the manager docstring.
  • Two regression tests, both verified to fail without the fix ([2, 1, 3] != [1, 2, 3]).

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.

4 participants