{"id":14909,"date":"2026-09-21T17:30:00","date_gmt":"2026-09-21T12:00:00","guid":{"rendered":"https:\/\/www.allerin.com\/blog\/?p=14909"},"modified":"2026-09-22T00:39:26","modified_gmt":"2026-09-21T19:09:26","slug":"rails-upgrade-checks-2026-09-18","status":"publish","type":"post","link":"https:\/\/www.allerin.com\/blog\/rails-upgrade-checks-2026-09-18\/","title":{"rendered":"Rails upgrade checks"},"content":{"rendered":"<div class=\"allerin-rails-reading\" style=\"overflow-wrap: break-word;\">\n<p>A successful boot and a green request test can miss the failures in the <a href=\"https:\/\/rubyonrails.org\/2026\/9\/18\/this-week-in-rails\" target=\"_blank\" rel=\"noopener\">18 September Rails update<\/a>. These Rails upgrade checks focus on PostgreSQL metadata, image-processing dependencies and the headers a streaming response actually sends.<\/p>\n<p>As checked on 18 September, the changes discussed below are on Rails&#8217; development branch, but absent from the latest published 8.1.3.1, 8.0.5.1 and 7.2.3.2 tags. A merge does not establish a released upgrade target.<\/p>\n<nav aria-label=\"Article sections\">Jump to <a href=\"#covering-indexes-can-hide-an-update\">covering indexes<\/a>, <a href=\"#a-table-name-must-resolve-to-one-table\">schema names<\/a>, <a href=\"#booting-does-not-prove-image-processing-works\">image processing<\/a>, or <a href=\"#test-the-streaming-response-path\">streaming headers<\/a>.<\/nav>\n<h2 id=\"covering-indexes-can-hide-an-update\" style=\"scroll-margin-top: 125px;\">Covering indexes can hide an update<\/h2>\n<p>PostgreSQL 11 added covering indexes in 2018. An <code style=\"overflow-wrap: anywhere;\">INCLUDE<\/code> column supplies data for an index-only scan without becoming part of the uniqueness key. Rails&#8217; primary-key inspection nevertheless read the index&#8217;s entire column list. <a href=\"https:\/\/github.com\/rails\/rails\/pull\/58555\" target=\"_blank\" rel=\"noopener\">PR #58555<\/a> changes the query to read the primary-key constraint&#8217;s own columns.<\/p>\n<p>That distinction matters to <code style=\"overflow-wrap: anywhere;\">upsert_all<\/code>, introduced in Rails 6.0 in 2019 for bulk writes without instantiating models. Its default update excludes primary-key columns. If a payload column is misidentified as a key, the operation can succeed while leaving that value unchanged.<\/p>\n<p>Our isolated check used Active Record 8.1.3.1, Ruby 3.3.3 and a fresh PostgreSQL 15.17 database. A primary key on <code style=\"overflow-wrap: anywhere;\">tenant_id<\/code> and <code style=\"overflow-wrap: anywhere;\">id<\/code>, with <code style=\"overflow-wrap: anywhere;\">name<\/code> included in its index, made Rails report all three as keys. An attempted upsert left <code style=\"overflow-wrap: anywhere;\">name<\/code> as <code style=\"overflow-wrap: anywhere;\">before<\/code>. Reading the constraint&#8217;s two key columns and correcting the cached primary-key metadata let the same operation write <code style=\"overflow-wrap: anywhere;\">after<\/code>. This tested the catalog query and a scoped metadata correction, not a complete checkout of Rails main.<\/p>\n<p>For an application adopting bulk upserts during an upgrade, assert the stored values after a conflict. Checking only that the operation returned successfully would have missed this case.<\/p>\n<h2 id=\"a-table-name-must-resolve-to-one-table\" style=\"scroll-margin-top: 125px;\">A table name must resolve to one table<\/h2>\n<p>A schema search path is ordered. If two PostgreSQL schemas contain a table called <code style=\"overflow-wrap: anywhere;\">products<\/code>, an unqualified name resolves to the first matching table. Rails&#8217; metadata queries could instead combine indexes and constraints from both. <a href=\"https:\/\/github.com\/rails\/rails\/pull\/58795\" target=\"_blank\" rel=\"noopener\">PR #58795<\/a> makes those queries inspect the single relation selected by PostgreSQL&#8217;s name resolution.<\/p>\n<p>The older adapter code is relevant here: the name-and-search-path query was already present in Rails 5.2, released in 2018. The new fix does not introduce multiple-schema support; it corrects how metadata is selected when names overlap.<\/p>\n<p>In the same disposable database, released Active Record returned indexes and check constraints from both schemas. Explicitly qualified names remained scoped. The revised index query returned only the first schema&#8217;s index, and reversing the search path changed the selected index.<\/p>\n<p>If your application switches schemas, include overlapping table names in its migration and metadata tests. A fixture with only one schema cannot expose this ambiguity.<\/p>\n<h2 id=\"booting-does-not-prove-image-processing-works\" style=\"scroll-margin-top: 125px;\">Booting does not prove image processing works<\/h2>\n<p>Active Storage arrived in Rails 5.2 in 2018 to connect applications to file storage. Rails 7.0&#8217;s defaults, released in 2021, selected libvips for image variants; an older application&#8217;s explicit processor setting can still differ. The July 2026 security patches added the native-library guard whose startup behavior these fixes refine.<\/p>\n<p>This week&#8217;s <a href=\"https:\/\/github.com\/rails\/rails\/pull\/58743\" target=\"_blank\" rel=\"noopener\">#58743<\/a> and <a href=\"https:\/\/github.com\/rails\/rails\/pull\/58734\" target=\"_blank\" rel=\"noopener\">#58734<\/a> address two startup failures. One handles a Ruby binding whose native libvips library is missing. The other raises when the vips processor loads or its analyzer reads an image, allowing an application using MiniMagick to start. It retains the protection against unfuzzed loaders; it does not make an old library acceptable for vips processing.<\/p>\n<p>These fixes concern loading and processing, rather than Marcel&#8217;s MIME identification or the content types already stored on blobs. Test application boot, image analysis and variant generation separately in the final deployment environment. A startup warning is not evidence that the worker can generate the image your users need.<\/p>\n<h2 id=\"test-the-streaming-response-path\" style=\"scroll-margin-top: 125px;\">Test the streaming response path<\/h2>\n<p>Rails 5.0, released in 2016, separated response construction from applying configured default headers. Ordinary responses used the constructor that adds them; <code style=\"overflow-wrap: anywhere;\">ActionController::Live<\/code> continued using the other path. <a href=\"https:\/\/github.com\/rails\/rails\/pull\/53403\" target=\"_blank\" rel=\"noopener\">PR #53403<\/a> corrects that choice.<\/p>\n<p>Our Action Pack 8.1.3.1 experiment returned a successful response and body under HTTP\/1.1 while omitting configured <code style=\"overflow-wrap: anywhere;\">nosniff<\/code>, <code style=\"overflow-wrap: anywhere;\">SAMEORIGIN<\/code> and a custom header. Replaying the one-line constructor change restored all three. The HTTP\/1.0 control already had them, which explains why a test using that protocol could pass without exercising the bug.<\/p>\n<p>For a Rails 5.2, 6.1 or 7.x application with streaming controllers, assert the required headers on the streaming path itself. Include Active Storage proxy endpoints where used, then check the deployed response as well as the controller test.<\/p>\n<h2 id=\"what-to-do-this-week\" style=\"scroll-margin-top: 125px;\">What to do this week<\/h2>\n<ul>\n<li>Assert updated values after a bulk-upsert conflict involving a covering primary key.<\/li>\n<li>Exercise duplicate table names across the schemas your application uses.<\/li>\n<li>Check boot, image analysis and variant generation as separate operations.<\/li>\n<li>Verify streaming headers with the request protocol made explicit.<\/li>\n<\/ul>\n<p>The <a href=\"https:\/\/rubyonrails.org\/2026\/9\/18\/this-week-in-rails\" target=\"_blank\" rel=\"noopener\">weekly update<\/a> credits 29 contributors for its <a href=\"https:\/\/contributors.rubyonrails.org\/contributors\/in-time-window\/20260911-20260918\" target=\"_blank\" rel=\"noopener\">11\u201318 September window<\/a>. Use the <a href=\"https:\/\/www.allerin.com\/services\/rails-upgrades\">Rails upgrades guide<\/a> to keep these checks within the next supported upgrade step. At Rails World, watch Andrew Novoselac&#8217;s discussion of global state and progress toward Rails Ractor safety; the <a href=\"https:\/\/rubyonrails.org\/world\/2026\/speakers\/andrew-novoselac\" target=\"_blank\" rel=\"noopener\">official speaker description<\/a> does not establish readiness for your application.<\/p>\n<h2 id=\"sources\" style=\"scroll-margin-top: 125px;\">Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/rubyonrails.org\/2026\/9\/18\/this-week-in-rails\" target=\"_blank\" rel=\"noopener\">This Week in Rails, 18 September 2026<\/a>, including its dated contributor window.<\/li>\n<li><a href=\"https:\/\/github.com\/rails\/rails\/releases\" target=\"_blank\" rel=\"noopener\">Rails release tags<\/a> and the five pull requests linked above.<\/li>\n<li><a href=\"https:\/\/www.postgresql.org\/docs\/release\/11.0\/\" target=\"_blank\" rel=\"noopener\">PostgreSQL 11 release notes<\/a> and <a href=\"https:\/\/guides.rubyonrails.org\/6_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">Rails 6.0 release notes<\/a>.<\/li>\n<li><a href=\"https:\/\/guides.rubyonrails.org\/5_2_release_notes.html\" target=\"_blank\" rel=\"noopener\">Rails 5.2 release notes<\/a> and <a href=\"https:\/\/guides.rubyonrails.org\/configuring.html#default-values-for-target-version-7-0\" target=\"_blank\" rel=\"noopener\">Rails 7.0 defaults<\/a>.<\/li>\n<li><a href=\"https:\/\/github.com\/rails\/rails\/commit\/e16afe61abd78c55f80752ca020b90d59ae1940f\" target=\"_blank\" rel=\"noopener\">Response-construction history<\/a>.<\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>A passing request and successful boot can miss an unchanged upsert value or absent streaming headers. Four checks drawn from the 18 September Rails update.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_links_to":"","_links_to_target":""},"categories":[2037],"tags":[2076,2042,2064,2038,2039],"class_list":["post-14909","post","type-post","status-publish","format-standard","hentry","category-ruby-on-rails","tag-actioncontroller-live","tag-active-storage","tag-postgresql","tag-rails-upgrades","tag-this-week-in-rails"],"_links":{"self":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14909","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/comments?post=14909"}],"version-history":[{"count":4,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14909\/revisions"}],"predecessor-version":[{"id":14968,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14909\/revisions\/14968"}],"wp:attachment":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/media?parent=14909"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/categories?post=14909"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/tags?post=14909"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}