List queries can end their sort on a column two rows share, and nothing checks the last sort key #225

Open
opened 2026-10-16 01:56:23 +00:00 by cgalo5758 · 1 comment
Owner

What a contributor runs into

A query that sorts on a column two rows can share returns those rows in whatever order the query plan produces, and that order can change from one run to the next. Seven paged operator lists (grants, products, organizations, people, billing accounts, subscriptions and domain claims) have been fixed by ending their sort on the row id, the shape ListEntitlementSetChanges uses: ORDER BY c.effective_at DESC, c.created_at DESC, c.change_id DESC (internal/entitlements/queries/entitlement_set_changes.sql:132). Ids are UUIDv7 (docs/identifiers.md:23) and rise in the order rows are written, so the id breaks a tie by write order.

These queries still end on a column that ties:

  • FedWiki's keep-and-park lists. ListActiveSitesByWorkspace, ListActiveSitesByPool and ListForceReducedSitesByPool (internal/integrations/fedwiki/store/queries/sites.sql:154, :165, :177) sort by last_modified_at DESC NULLS LAST, created_at DESC. A downgrade keeps the first sites of the pool's list and parks the rest, and an upgrade reactivates parked sites in list order (reconcilePoolQuota, internal/integrations/fedwiki/workflows/reconcile.go:154-185). A swap parks the last site of the workspace's list (swapActiveSite, internal/integrations/fedwiki/web/site_actions.go:592-603). created_at defaults to NOW() (internal/integrations/fedwiki/store/migrations/00001_init.sql:41), the start of the writing transaction, so two sites written in one transaction with the same last_modified_at, or none, tie. I have not found a production path that writes two sites in one transaction, so this is a gap in the query, not an observed failure.
  • ListOrganizationsByType (internal/organization/queries/organizations.sql:90) sorts by name alone; what that does to the default change and the reorder is described on #187.
  • ListAllProducts (internal/billing/queries/products.sql:28) and ListSharedTierProducts (internal/billing/queries/plan_ladders.sql:130) sort by product name, which is not unique. The plan topology overview (internal/server/operator_topology.go:124-140) lists same-named products, shared tiers and add-ons alike, in either order.
  • SearchOrganizationsByName (internal/organization/queries/organizations.sql:84) and LookupPersons (internal/identity/queries/persons.sql:42-44) sort by name and stop at a limit. When more rows match than the limit, which ones the operator lookup lists is not defined.
  • ListDeliveringGrantsByOrgID (internal/entitlements/queries/grants.sql:364) sorts by created_at alone. Grants written in one transaction share it, so a member's Sources list can show them in either order.
  • ListEntitlementSetChangeEffects (internal/entitlements/queries/entitlement_set_changes.sql:149) sorts one rule change's effects by organization name and key, and two organizations with one name tie. Only a test calls it today; a review of a rule change's effects (#113) would read it.
  • ListFailedObligations (internal/entitlements/queries/entitlement_set_changes.sql:101) ends on o.pool_id, which looks unique and is not: one pool can have failed obligations under two changes of the same set, and the query stops at a limit.

Three cases have their own issues: the organization page's members list (#210), the rows core.confer writes (#218), and sign-in taking the first organization and workspace by name (#122).

Why it costs

A tie shows up as a golden test that fails now and then, a page that reorders on refresh, or a pager that lists one row on two pages and another on none. People learn to rerun, and a real change in order then gets rerun too. In the FedWiki lists the tie decides which of a member's sites goes read-only. Each fix so far was made by hand, one query at a time, and nothing stops the next query from repeating the mistake.

A rough scan, a script that reads the last key of each ORDER BY, finds 163 ORDER BY clauses in the 61 tracked queries/*.sql files. 143 of them order a query's own result, and 87 of those end on a column not named id or *_id, most often a name (19), created_at (18) or resource_key (13). Many of the 87 are unique already (a domain, a key, a provider name), and an ending on *_id is not always unique, as ListFailedObligations shows. A check on the column's name alone is therefore not enough.

Where

Every queries/*.sql file beside a package's sqlc.yaml; the project lint (internal/lint, run by go run . lint from make lint); docs/go-conventions.md.

Done when

  • The queries listed above end their ORDER BY on a unique key: the row id where no other column is unique.
  • make lint fails on a query whose ORDER BY does not end on a unique key, unless an allowlist names the query with a reason (<package path>.<Name> # reason), the way scripts/sqlc-unused/allow.txt names the unused queries that stay. An entry whose query was fixed or deleted fails too. A first version can accept a last key named id or *_id and require an entry for every other ending. A stricter version reads the primary keys and unique constraints the migrations declare, so it also catches an ending like ListFailedObligations' pool_id. scripts/sqlc-unused already finds every package's query files, and the rule can share that walk.
  • Removing the id from the end of one of the seven fixed lists fails the lint.
  • docs/go-conventions.md states the rule and names the check.

Related: #165 reworks the FedWiki paths that change a site's status, the swap and the downgrade sweep among them, which read the three site lists.

## What a contributor runs into A query that sorts on a column two rows can share returns those rows in whatever order the query plan produces, and that order can change from one run to the next. Seven paged operator lists (grants, products, organizations, people, billing accounts, subscriptions and domain claims) have been fixed by ending their sort on the row id, the shape `ListEntitlementSetChanges` uses: `ORDER BY c.effective_at DESC, c.created_at DESC, c.change_id DESC` (`internal/entitlements/queries/entitlement_set_changes.sql:132`). Ids are UUIDv7 (`docs/identifiers.md:23`) and rise in the order rows are written, so the id breaks a tie by write order. These queries still end on a column that ties: - **FedWiki's keep-and-park lists.** `ListActiveSitesByWorkspace`, `ListActiveSitesByPool` and `ListForceReducedSitesByPool` (`internal/integrations/fedwiki/store/queries/sites.sql:154`, `:165`, `:177`) sort by `last_modified_at DESC NULLS LAST, created_at DESC`. A downgrade keeps the first sites of the pool's list and parks the rest, and an upgrade reactivates parked sites in list order (`reconcilePoolQuota`, `internal/integrations/fedwiki/workflows/reconcile.go:154-185`). A swap parks the last site of the workspace's list (`swapActiveSite`, `internal/integrations/fedwiki/web/site_actions.go:592-603`). `created_at` defaults to `NOW()` (`internal/integrations/fedwiki/store/migrations/00001_init.sql:41`), the start of the writing transaction, so two sites written in one transaction with the same `last_modified_at`, or none, tie. I have not found a production path that writes two sites in one transaction, so this is a gap in the query, not an observed failure. - **`ListOrganizationsByType`** (`internal/organization/queries/organizations.sql:90`) sorts by name alone; what that does to the default change and the reorder is described on #187. - **`ListAllProducts`** (`internal/billing/queries/products.sql:28`) and **`ListSharedTierProducts`** (`internal/billing/queries/plan_ladders.sql:130`) sort by product name, which is not unique. The plan topology overview (`internal/server/operator_topology.go:124-140`) lists same-named products, shared tiers and add-ons alike, in either order. - **`SearchOrganizationsByName`** (`internal/organization/queries/organizations.sql:84`) and **`LookupPersons`** (`internal/identity/queries/persons.sql:42-44`) sort by name and stop at a limit. When more rows match than the limit, which ones the operator lookup lists is not defined. - **`ListDeliveringGrantsByOrgID`** (`internal/entitlements/queries/grants.sql:364`) sorts by `created_at` alone. Grants written in one transaction share it, so a member's Sources list can show them in either order. - **`ListEntitlementSetChangeEffects`** (`internal/entitlements/queries/entitlement_set_changes.sql:149`) sorts one rule change's effects by organization name and key, and two organizations with one name tie. Only a test calls it today; a review of a rule change's effects (#113) would read it. - **`ListFailedObligations`** (`internal/entitlements/queries/entitlement_set_changes.sql:101`) ends on `o.pool_id`, which looks unique and is not: one pool can have failed obligations under two changes of the same set, and the query stops at a limit. Three cases have their own issues: the organization page's members list (#210), the rows `core.confer` writes (#218), and sign-in taking the first organization and workspace by name (#122). ## Why it costs A tie shows up as a golden test that fails now and then, a page that reorders on refresh, or a pager that lists one row on two pages and another on none. People learn to rerun, and a real change in order then gets rerun too. In the FedWiki lists the tie decides which of a member's sites goes read-only. Each fix so far was made by hand, one query at a time, and nothing stops the next query from repeating the mistake. A rough scan, a script that reads the last key of each ORDER BY, finds 163 ORDER BY clauses in the 61 tracked `queries/*.sql` files. 143 of them order a query's own result, and 87 of those end on a column not named `id` or `*_id`, most often a name (19), `created_at` (18) or `resource_key` (13). Many of the 87 are unique already (a domain, a key, a provider name), and an ending on `*_id` is not always unique, as `ListFailedObligations` shows. A check on the column's name alone is therefore not enough. ## Where Every `queries/*.sql` file beside a package's `sqlc.yaml`; the project lint (`internal/lint`, run by `go run . lint` from `make lint`); `docs/go-conventions.md`. ## Done when - The queries listed above end their ORDER BY on a unique key: the row id where no other column is unique. - `make lint` fails on a query whose ORDER BY does not end on a unique key, unless an allowlist names the query with a reason (`<package path>.<Name> # reason`), the way `scripts/sqlc-unused/allow.txt` names the unused queries that stay. An entry whose query was fixed or deleted fails too. A first version can accept a last key named `id` or `*_id` and require an entry for every other ending. A stricter version reads the primary keys and unique constraints the migrations declare, so it also catches an ending like `ListFailedObligations`' `pool_id`. `scripts/sqlc-unused` already finds every package's query files, and the rule can share that walk. - Removing the id from the end of one of the seven fixed lists fails the lint. - `docs/go-conventions.md` states the rule and names the check. Related: #165 reworks the FedWiki paths that change a site's status, the swap and the downgrade sweep among them, which read the three site lists.
cgalo5758 added the
kind
debt
area/fedwikiarea/testingarea/meta
priority
medium
labels 2026-10-16 01:56:23 +00:00
Author
Owner

FedWiki's three lists, ListActiveSitesByWorkspace, ListActiveSitesByPool and ListForceReducedSitesByPool, now end on site_id DESC, so two sites that tie on every other column come back in one order and the sweep parks the same one every pass (9027c0b1). The lint rule and the other lists remain.

FedWiki's three lists, `ListActiveSitesByWorkspace`, `ListActiveSitesByPool` and `ListForceReducedSitesByPool`, now end on `site_id DESC`, so two sites that tie on every other column come back in one order and the sweep parks the same one every pass (9027c0b1). The lint rule and the other lists remain.
cgalo5758 referenced this issue from a commit 2026-10-22 18:40:40 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#225