Skip to content

Implementation/COMMS-1046: Labels in the work package schema and labels-by-workspace API - #25552

Open
akabiru wants to merge 8 commits into
devfrom
implementation/comms-1046-labels-schema-and-workspace-listing
Open

akabiru wants to merge 8 commits into
devfrom
implementation/comms-1046-labels-schema-and-workspace-listing

Conversation

@akabiru

@akabiru akabiru commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

https://community.openproject.org/wp/COMMS-1046

Adds labels to the work package schema behind the work_package_labels flag, linking its allowed values to a new GET /api/v3/workspaces/:id/labels that orders labels by relevance for the workspace: labels already used there first, then the most used overall, then by name.

Label assignments now take part in the work package cache checksum, so a labels-only change is no longer served stale, and exports print label names while the flag hides the attribute from form configuration and exports when off. Nested collection endpoints also honour offset and pageSize, which they silently ignored before.

AI involvement

Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.

@akabiru
akabiru added this pull request to stack #25555 September 23, 2026 20:57
@akabiru akabiru changed the title implementation/comms 1046 labels schema and workspace listing Implementation/COMMS-1046: Labels in the work package schema and labels-by-workspace API Sep 23, 2026
@akabiru akabiru self-assigned this Sep 23, 2026
@akabiru akabiru added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 23, 2026
@akabiru akabiru added this to the 18.0.x milestone Sep 23, 2026
@akabiru
akabiru force-pushed the implementation/comms-1046-labels-schema-and-workspace-listing branch from 5e5d36c to 69115b2 Compare September 24, 2026 06:13
@github-actions github-actions Bot removed the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@akabiru
akabiru force-pushed the implementation/comms-1046-labels-schema-and-workspace-listing branch 2 times, most recently from af7e6a5 to 934a475 Compare September 24, 2026 13:39
@akabiru akabiru added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@github-actions github-actions Bot removed the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/notifications/navigation_spec.rb[1:1:1]
  • rspec ./spec/features/roles/report_spec.rb[1:2]
  • rspec ./spec/features/roles/report_spec.rb[1:3]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #25552, linked for reference only):

- `rspec ./spec/features/notifications/navigation_spec.rb[1:1:1]`
- `rspec ./spec/features/roles/report_spec.rb[1:2]`
- `rspec ./spec/features/roles/report_spec.rb[1:3]`

Treat this as a standalone task, unrelated to PR #25552. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #25552 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @akabiru to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @akabiru, and request a review from @akabiru.
On every commit, set @akabiru as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@akabiru akabiru added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@github-actions github-actions Bot removed the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@akabiru
akabiru marked this pull request as ready for review September 24, 2026 14:42
@akabiru akabiru added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@github-actions github-actions Bot removed the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
@akabiru
akabiru requested a review from a team September 24, 2026 14:43
Comment thread app/models/label.rb
Comment on lines +41 to +52
scope :ordered_by_relevance_for, ->(project) {
used_in_project = Labeling
.where(labelable_type: WorkPackage.name)
.where("labelings.label_id = labels.id")
.joins("INNER JOIN work_packages ON work_packages.id = labelings.labelable_id")
.where(work_packages: { project_id: project })
.arel
.exists

reorder(used_in_project.desc)
.order(Arel.sql("#{USAGE_COUNT_SQL} DESC"))
.order("LOWER(labels.name) ASC")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 Benchmark: Label.ordered_by_relevance_for

The labels dropdown calls this endpoint with pageSize=-1 (clamped to apiv3_max_page_size, 1000 by default) and re-queries with a name ~ filter on each keystroke, so I benchmarked the exact SQL the index endpoint runs at three data sizes. Data was bulk-inserted with a skewed labeling distribution, the target project holding ~1-2% of work packages. Timings are server-side query times in ms (median / p95 over 19 warm runs).

Scenario (labels / labelings / WPs) Unfiltered Filtered total count Alphabetical baseline
Small (100 / 10k / 5k) 4.7 / 68.7 0.7 / 1.9 0.3 1.9
Medium (1k / 100k / 50k) 97.2 / 114.5 1.2 / 2.6 0.3 31.6
Large (10k / 1M / 500k) 627.5 / 841.8 5.0 / 5.9 0.6 274.2

Where the time goes. The "used in this project" EXISTS is cheap: Postgres turns it into a hashed subplan that runs once (loops=1). The cost is the correlated usage_count subquery, which runs once per label (loops=10000, ~1M buffer hits at the large size). The name filter is applied before those subqueries are evaluated, so the per-keystroke filtered path stays in single-digit ms even at the large size. Only the first, unfiltered open of the dropdown gets slow, and only on instances with thousands of labels and ~1M labelings.

Alternatives tried (large size only):

Rewrite Unfiltered Filtered
EXISTS as a CTE ~623 not run
usage_count via one GROUP BY ~227 not run
Both combined ~90 ~87

The combined rewrite returns the same order and brings the unfiltered open under 100ms, but it makes every filtered keystroke ~17x slower (5ms to 87ms), because aggregating over all labelings is a fixed cost the name filter can't reduce.

Decision: keeping the current query. The keystroke path is fast at every size, and the slow case is a first open on very large instances. If that becomes a real problem, the GROUP BY rewrite can be applied to the unfiltered request only.

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.

Nice. This gives confidence. Thanks for making it.

@akabiru akabiru added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Sep 24, 2026
Labels are global and unbounded, so allowed values are linked to a
project-scoped endpoint rather than embedded. The flag joins the schema
cache dependencies so toggling it does not serve stale schemas.
The work package labels dropdown needs labels already used in the current
workspace first, then the most used ones, so the schema points at this
endpoint instead of the global listing.
Assigning labels does not touch the work package row, so without this
the cached JSON representation kept serving the previous labels.
The form attribute list is derived from the schema representer's static
definitions, where show_if never runs, so exports and the type form
configuration listed labels with the flag off and printed the raw
association with it on.
Nothing reads usage_count from ordered_by_relevance_for, and plain SQL
strings read easier than the Arel expressions here.
The shared index endpoint already handles offset and pageSize; reorder lets
the relevance ordering replace the query's default name order on merge.
New nested resources only get workspace-scoped path helpers; the projects route remains an undocumented alias.
@akabiru
akabiru force-pushed the implementation/comms-1046-labels-schema-and-workspace-listing branch from c1f84a6 to 389bd60 Compare September 29, 2026 10:01

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

Nice job. This looks really good 👍

Approving, as it can go through, but left a comment in case you want to think about naming shenanigans 😁

Cheers 🎉 🎉 🎉

Comment thread app/models/label.rb
Comment on lines +41 to +52
scope :ordered_by_relevance_for, ->(project) {
used_in_project = Labeling
.where(labelable_type: WorkPackage.name)
.where("labelings.label_id = labels.id")
.joins("INNER JOIN work_packages ON work_packages.id = labelings.labelable_id")
.where(work_packages: { project_id: project })
.arel
.exists

reorder(used_in_project.desc)
.order(Arel.sql("#{USAGE_COUNT_SQL} DESC"))
.order("LOWER(labels.name) ASC")

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.

Nice. This gives confidence. Thanks for making it.

Comment thread app/models/label.rb
select("labels.*, #{USAGE_COUNT_SQL} AS usage_count")
}

scope :ordered_by_relevance_for, ->(project) {

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.

🟢 I am debating this with myself, but I feel there's something a bit off

  • relevance is ambiguous, it can mean anything and it does not explain what the sorting is doing
  • reading the code explains it, but it takes a minute to parse everything
  • the product solution is not really tested and might change in the future

For me either add an explanation comment...

# Sorts first by locality (labels already used in the current project come first)
# then by frequency (most used first).

... or make the scope name overexplain itself

scope :ordered_by_locality_and_usage_for, -> (project) {

but if this changes in the future we need to change the method signature 👀

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line.

Development

Successfully merging this pull request may close these issues.

2 participants