Fix Attachment#deletable? false positives#3855
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3855 +/- ##
==========================================
+ Coverage 98.06% 98.07% +0.01%
==========================================
Files 322 323 +1
Lines 8530 8582 +52
==========================================
+ Hits 8365 8417 +52
Misses 165 165 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Extracts the shared polymorphic-reference subquery from RelatableResource into a constant so including classes can reuse it if they oeverride the .deletable scope, because you can't call super in scopes.
The deletable scope only considered polymorphic related_object references, which missed attachments linked from /attachment/:id/download URLs written into ingredient values by the file tab of the link dialog. On sites where attachments are linked from Richtext, Link, or Html ingredients, every attachment was reported as deletable, risking accidental data loss. The scope now adds a correlated NOT EXISTS against ingredient value columns. The per-row LIKE pattern is built with Arel::Nodes::Concat so it compiles to || on SQLite and PostgreSQL and to CONCAT() on MySQL without adapter-specific SQL. The /download suffix in the pattern delimits the id, so matching attachment 1 does not incorrectly pull in URLs pointing at attachment 10.
88c3c0c to
3f0bc74
Compare
sascha-karnatz
approved these changes
Apr 27, 2026
Rendering the attachment index used to call Attachment#deletable? per row to decide which delete buttons to enable. Each call issued up to two queries (the polymorphic related_ingredients check and the LIKE scan for ingredient-value references), so a page of N attachments cost up to 2N extra queries in addition to the main fetch. The controller now runs the deletable scope once for the ids on the current page and stores the result in a Set. Because the scope relation is chained as the subquery source of an IN clause, Rails applies the same Ransack filter, sort, and pagination on the database side without materializing the ids Ruby-side, keeping the check tight to the rows the view actually renders.
Rendering the picture archive used to call Picture#deletable? per row to decide which delete buttons to show. Each call issued up to two queries (the polymorphic related_ingredients check and the LIKE scan for ingredient-value references), so a page of N pictures cost up to 2N extra queries in addition to the main fetch. The controller now runs the deletable scope once for the ids on the current page and stores the result in a Set. Because the scope relation is chained as the subquery source of an IN clause, Rails applies the same Ransack filter, sort, and pagination on the database side without materializing the ids Ruby-side, keeping the check tight to the rows the view actually renders. The same preload is wired to the update action since it re-renders the picture partial via turbo stream.
3f0bc74 to
f32d773
Compare
💔 Some backports could not be created
Manual backportTo create the backport manually run: Questions ?Please refer to the Backport tool documentation and see the Github Action logs for details |
Collaborator
💚 All backports created successfully
Questions ?Please refer to the Backport tool documentation |
Collaborator
💚 All backports created successfully
Questions ?Please refer to the Backport tool documentation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this pull request for?
Fixes a bug where
Alchemy::Attachment#deletable?(and the corresponding.deletablescope) would incorrectly treat attachments as deletable when they were referenced inside ingredient values other than polymorphicrelated_object— for example a download URL pasted into a Richtext body, or a Link ingredient pointing at the attachment's download path.On top of the fix, both the attachments and pictures admin archives used to call
deletable?per row, each issuing up to two extra queries. The controllers now preload the deletable ids for the current page in a single query scoped via a subquery, so filter, sort, and pagination are applied on the database side.Notable changes
Alchemy::Attachment.deletablenow also excludes rows whose download URL pattern (%/attachment/<id>/download%) appears in any ingredient'svaluecolumn, usingArel::Nodes::Concatso it works across SQLite, PostgreSQL, and MySQL.Alchemy::Attachment#deletable?gains a secondary check (referenced_in_ingredient_value?) usingLIKEon the concrete id, acting as a cheap short-circuit after the polymorphic check.RelatableResource::RELATED_INGREDIENTS_SUBQUERYextracted as a reusable SQL constant.Alchemy::Admin::AttachmentsController#indexandAlchemy::Admin::PicturesController(viabefore_actions) preload@deletable_attachment_ids/@deletable_picture_idsas aSet; the_pictureand_files_listpartials check membership instead of callingdeletable?per row.Checklist