Skip to content

fix(agent): keep the search and segments of a select-all in delete, actions and dissociate [PRD-1667] - #414

Merged
bexchauveto merged 4 commits into
mainfrom
feature/prd-1667-agent-ruby-delete-all-on-a-searched-list-deletes-the-whole
Oct 20, 2026
Merged

bexchauveto merged 4 commits into
mainfrom
feature/prd-1667-agent-ruby-delete-all-on-a-searched-list-deletes-the-whole

Conversation

@bexchauveto

@bexchauveto bexchauveto commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Fixes PRD-1667: Delete all on a searched list deleted the whole collection.

What changes

  1. Bulk delete. Before: a select-all ignored the list's search, segment and live-query segment, so Delete all on 25 matching tasks deleted all 28. Polymorphic references were also cleared from the request's ids before the filter existed, so on a select-all they were cleared for the excluded records. After: it deletes only the records the list showed, minus the excluded ones, and clears references only for the records it lists through that same filter.
  2. Bulk actions. Before: a select-all action ran on every record of the collection. After: it runs on the searched/segmented list only.
  3. Dissociate / delete on a related list. Before: same gap on a select-all. After: same fix.
  4. Shared filter. Before: each route assembled its filter by hand, and delete, actions and dissociate skipped the read-permission check on the search. After: delete, actions, dissociate, list, count and csv, and the related list, count and csv, all build it through QueryHandler#build_context_filter, the Ruby counterpart of Node's ContextFilterFactory.build, which also runs assert_can_read_query_fields. parse_query_segment and parse_segment also read all_records_subset_query. A searched related list now shows only what a select-all delete on it reaches, and related count applies the list's filters too.
  5. ActiveRecord condition trees. Before: And[a, Or[x, y]] compiled as a OR x OR y, so the search Or widened a searched Delete all to the whole collection (and already widened a searched, filtered list on main). After: each branch is its own relation, which ActiveRecord groups. A failed delete also no longer leaves polymorphic children detached: the references are nullified after the delete succeeds.

Decisions

  • PRD-1667: 2 functional, 2 architectural · all ticked

Definition of Done

General

  • Write an explicit title for the Pull Request, following Conventional Commits specification
  • Test manually the implemented changes
  • Validate the code quality (indentation, syntax, style, simplicity, readability)

Security

  • Consider the security impact of the changes made

Squash message

fix(agent): keep the search and segments of a select-all in delete, actions and dissociate

A select-all request carries the list's search, segment and live-query
segment in all_records_subset_query. Bulk delete, bulk actions and
dissociate/delete-related ignored them, so "Delete all" on a searched list
deleted the whole collection, and delete cleared polymorphic references
of the excluded records. Every route now builds its filter, read-permission
check included, through QueryHandler#build_context_filter, and delete clears
references only after deleting the records that filter lists.

The ActiveRecord datasource compiled And[a, Or[x, y]] as a OR x OR y,
which widened any searched filter to the whole collection. Each branch is
now built as its own relation, which ActiveRecord groups.

🤖 Generated with Claude Code

Note

Fix delete, actions and dissociate to keep search and segments of a select-all

  • Select-all requests (bulk delete, actions, dissociate) previously dropped the search, searchExtended, and segment values, so the operation ran on more records than the user selected
  • Adds a shared build_context_filter helper in query_handler.rb that combines the permission scope, request condition tree, and optional segment native-query condition tree, and carries search and segment metadata on the Filter
  • Segment native queries can now be supplied in the select-all subset query; QueryStringParser.parse_segment and QueryHandler.parse_query_segment resolve them with a fallback to the query-string segment
  • Polymorphic cleanup in delete.rb now nullifies references only for records matching the final deletion filter, and skips the foreign-collection update when no matching origin values exist
  • Behavioral Change: an unknown segment in a select-all delete now raises BadRequestError before deletion; polymorphic nullification is driven by a filtered list with an origin-key projection instead of raw selection IDs

Changes since #414 opened

  • Fixed nested condition tree SQL generation in ActiveRecord datasource [b9b6b40]
  • Centralized filter construction across route handlers using build_context_filter [b9b6b40]
  • Modified build_context_filter to accept sort and validate all query fields upfront [b9b6b40]
  • Reordered polymorphic reference nullification to occur after deletion [b9b6b40]
  • Updated test expectations for search_extended and permissions validation [b9b6b40]
  • Updated related resource route handlers to apply context-aware filters [76957e2]
  • Updated related resource route handler tests to verify context-aware filtering [76957e2]

Macroscope summarized 5640593.

…ctions and dissociate [PRD-1667]

A select-all request carries the list's search, segment and live-query
segment in all_records_subset_query. Bulk delete, bulk actions and
dissociate/delete-related built their filter from the condition tree,
the ids and the scope only, so "Delete all" on a searched list deleted
the whole collection.

The three routes now build their filter through one
QueryHandler#build_context_filter, as the Node agent does with
ContextFilterFactory.build. parse_query_segment and parse_segment read
the select-all body too.

Refs: PRD-1667

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 16, 2026

Copy link
Copy Markdown

PRD-1667

@qltysh

qltysh Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on main by 0.1%.

Modified Files with Diff Coverage (12)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
..._admin_agent/lib/forest_admin_agent/routes/resources/delete.rb100.0%
Coverage rating: A Coverage rating: A
...est_admin_agent/lib/forest_admin_agent/routes/query_handler.rb100.0%
Coverage rating: B Coverage rating: A
...ecord/lib/forest_admin_datasource_active_record/utils/query.rb75.0%119-122, 127...
Coverage rating: A Coverage rating: A
...st_admin_agent/lib/forest_admin_agent/routes/resources/list.rb100.0%
Coverage rating: A Coverage rating: A
...est_admin_agent/routes/resources/related/dissociate_related.rb100.0%
Coverage rating: A Coverage rating: A
...b/forest_admin_agent/routes/resources/related/count_related.rb100.0%
Coverage rating: A Coverage rating: A
...ib/forest_admin_agent/routes/resources/related/list_related.rb100.0%
Coverage rating: A Coverage rating: A
...est_admin_agent/lib/forest_admin_agent/routes/resources/csv.rb100.0%
Coverage rating: A Coverage rating: A
...dmin_agent/lib/forest_admin_agent/utils/query_string_parser.rb100.0%
Coverage rating: A Coverage rating: A
...lib/forest_admin_agent/routes/resources/related/csv_related.rb100.0%
Coverage rating: A Coverage rating: A
...st_admin_agent/lib/forest_admin_agent/routes/action/actions.rb100.0%
Coverage rating: A Coverage rating: A
...t_admin_agent/lib/forest_admin_agent/routes/resources/count.rb100.0%
Total89.2%
🤖 Increase coverage with AI coding...
In the `feature/prd-1667-agent-ruby-delete-all-on-a-searched-list-deletes-the-whole` branch, add test coverage for this new code:

- `packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb` -- Lines 119-122, 127, 132-137, and 164-170

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

Comment thread packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb Outdated
…ete matches [PRD-1667]

The cleanup ran before the delete filter was built and from the request's
ids, so a rejected filter still cleared references, and a select-all
cleared those of the excluded records instead of the deleted ones. It now
lists the records through the final filter first. A select-all body
sending segment: false falls back to the query string segment again.

Refs: PRD-1667

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bexchauveto
bexchauveto marked this pull request as ready for review October 16, 2026 14:35

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

Spec (PRD-1667): not met in every case. The ticket holds the gap.

Comment thread packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb Outdated
Comment thread packages/forest_admin_agent/lib/forest_admin_agent/routes/query_handler.rb Outdated
Comment thread packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb Outdated
…nswer the PRD-1667 review [PRD-1667]

ActiveRecord chained every leaf onto the running query, so And[a, Or[x, y]]
compiled as `a OR x OR y`: the search Or that select-all now carries into
delete widened it to the whole collection, and already widened a searched
list on main. Each branch is now built as its own relation, which
ActiveRecord groups.

build_context_filter checks the caller can read the fields its search and
filter reach, and list, count and csv build through it too. Delete lists
the polymorphic keys, deletes, then nullifies, so a failed delete leaves the
children attached.

Refs: PRD-1667

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qltysh

qltysh Bot commented Oct 16, 2026

Copy link
Copy Markdown

3 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 5): condition_relation 2
qlty Structure Function with many parameters (count = 5): build_context_filter 1

condition_tree_segment
end

def build_context_filter(collection, args, permissions, caller, sort: nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Function with many parameters (count = 5): build_context_filter [qlty:function-parameters]

relations = condition_tree.conditions.map { |condition| condition_relation(condition) }
return @collection.model.where(Arel.sql(aggregator == :or ? '1 = 0' : '1 = 1')) if relations.empty?

relations.reduce { |combined, relation| combined.send(aggregator, relation) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Function with high complexity (count = 5): condition_relation [qlty:function-complexity]

length_func = Arel::Nodes::NamedFunction.new('LENGTH', [arel_attr])
@query = query_aggregator(aggregator, @collection.model.where(length_func.gt(value)))
@collection.model.where(length_func.gt(value))
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Function with high complexity (count = 14): leaf_relation [qlty:function-complexity]

selected_ids
]
)
filter = build_context_filter(context.child_collection, args, context.permissions, context.caller)

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.

Claude Opus 5.5 (claude-opus-5-5), from manual testing at b9b6b40: a select-all delete on a searched related list now deletes fewer records than the user selected, because the related list itself ignores the search.

  1. Open a project's Tasks related list and search brief. The list still shows every task, Fuel the rocket included.
  2. Select all records, untick one, then Delete the tasks. The confirm says 3 tasks.
  3. Only the 2 brief tasks are deleted. Fuel the rocket survives.

Before this PR the same action deleted all 3, since the related list and the delete both ignored the search. Now the delete honours it through build_context_filter here, while list_related.rb:34, count_related.rb:28 and csv_related.rb:33 still build their filter from the scope and filters only, never search or segment. Node's related list, count and csv go through ContextFilterFactory.build.

A way out: build those three filters with build_context_filter, as list, count and csv now do.

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.

Claude Opus 5.5 (claude-opus-5-5): Related list, count and csv now build their filter through build_context_filter, so they show and count what a select-all delete reaches.
76957e2

Why

Each route has a spec where a search and a segment reach the related filter.
Related count also applies the list's filters now, which it used to drop.

…sv [PRD-1667]

A select-all delete on a searched related list now honours the search,
but the related list, count and csv still ignored it, so the list showed
records the delete would not touch. The three routes now build their
filter through build_context_filter, as Node does with
ContextFilterFactory.build. Related count also applies the list's
filters, which it used to drop.

Refs: PRD-1667

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

Approved after /verify-fixes run 414-20261020124244-hercemer42-verify: 6 findings of 414-20261016144136-hercemer42 closed or accepted, none reopened.

@bexchauveto
bexchauveto merged commit 8d4db0f into main Oct 20, 2026
33 checks passed
@bexchauveto
bexchauveto deleted the feature/prd-1667-agent-ruby-delete-all-on-a-searched-list-deletes-the-whole branch October 20, 2026 12:53
forest-bot added a commit that referenced this pull request Oct 20, 2026
## [1.46.2](v1.46.1...v1.46.2) (2026-10-20)

### Bug Fixes

* **agent:** keep the search and segments of a select-all in delete, actions and dissociate [PRD-1667] ([#414](#414)) ([8d4db0f](8d4db0f))
@forest-bot

Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.46.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants