Skip to content

Add setting for enumerating column names in SELECT statements#41718

Merged
rafaelfranca merged 1 commit intorails:mainfrom
dzunk:enumerate_columns
Apr 15, 2021
Merged

Add setting for enumerating column names in SELECT statements#41718
rafaelfranca merged 1 commit intorails:mainfrom
dzunk:enumerate_columns

Conversation

@dzunk
Copy link
Contributor

@dzunk dzunk commented Mar 21, 2021

Summary

Add a new configuration setting ActiveRecord::Base.enumerate_columns_in_select_statements. When truthy, ActiveRecord will explicitly project all column names in SELECT statements, rather than a wildcard table_name.*.

E.g. SELECT * FROM posts becomes SELECT id, title, body, created_at, updated_at FROM posts

Other Information

When a column is added to a Postgres table, it changes the query result for any wildcard SELECTs. This causes PreparedStatementCacheExpired errors when the statement occurs inside a transaction. #22170 cleans up the statement cache, but the error is still raised and will fail the transaction, and probably cause a 500 error, failed background job, etc.

I've seen a few different recommendations on how to handle these:

The last option above works because setting any value for ignored_columns triggers the code path to project all column names instead of a wildcard, and this is what we're currently using in production.

# app/models/application_record.rb
class ApplicationRecord
  self.ignored_columns = [:__fake_column__]
end

However, this feels hacky, and isn't how ignored_columns is supposed to be used. Adding an option to enable this behavior all the time seems cleaner, and makes it easier to opt-in on a per-model, or per-environment basis.

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.active_record.enumerate_columns_in_select_statements = true
  end
end

If the code looks good and is agreeable to everyone, I'll make sure to update the docs and add a changelog entry as well.

@rafaelfranca
Copy link
Member

This makes sense to me. Just a small comment that we should use class_attribute.

@rails-bot rails-bot bot added the docs label Mar 28, 2021
@dzunk dzunk force-pushed the enumerate_columns branch 4 times, most recently from 7cfeef4 to 28db742 Compare March 31, 2021 15:13
@dzunk dzunk force-pushed the enumerate_columns branch from 28db742 to b3ea8aa Compare April 13, 2021 17:50
@dzunk dzunk requested a review from rafaelfranca April 14, 2021 18:42
@dzunk dzunk force-pushed the enumerate_columns branch from b3ea8aa to 726abea Compare April 14, 2021 22:48
@rafaelfranca rafaelfranca merged commit 7ce853e into rails:main Apr 15, 2021
rafaelfranca added a commit that referenced this pull request Apr 15, 2021
@dzunk dzunk deleted the enumerate_columns branch April 16, 2021 04:33
@uberllama
Copy link
Contributor

This is a brilliant addition to finally and cleanly deal with a real world pain, which anyone working on a highly transactional system deals with regularly. Cheers. 👍

@elspethsoup
Copy link

It would be awesome if this got back ported to rails 6.1.


* `config.active_record.queues.destroy` allows specifying the Active Job queue to use for destroy jobs. When this option is `nil`, purge jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). It defaults to `nil`.

* `config.active_record.enumerate_columns_in_select_statements` when truthy, will always include column names in `SELECT` statements, and avoid wildcard `SELECT * FROM ...` queries. This avoids prepared statement cache errors when adding columns to a Postgres database. Defaults to `false`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, but why does it default to false? Is there any drawback to enumerating the columns instead of using *?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I know there's no drawback, but it's off by default to guarantee backwards compatibility by maintaining existing behavior.

Copy link
Contributor

Choose a reason for hiding this comment

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

One small drawback is that the query logs will be bigger/more verbose when showing the SQL generated by ActiveRecord.

Copy link

@atagunay atagunay left a comment

Choose a reason for hiding this comment

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

thanks man !!!

@danielvdao
Copy link

danielvdao commented Jun 7, 2023

I took a look at the Rails backporting policy and would be curious to know if this feature could be backported to Rails 6 🙇🏽‍♂️

Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a Rails team member before backporting your changes to avoid wasted effort.

While this seems like a feature, I feel like this is a bug, considering that it's taken down high traffic Rails applications. For us it's shot developers in the foot when running migrations that touch a table with a query in a transaction. As a result we've had to do multiple force deploys, but that seems like a workaround as opposed to this PR.

@uberllama
Copy link
Contributor

I took a look at the Rails backporting policy and would be curious to know if this feature could be backported to Rails 6 🙇🏽‍♂️

Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a Rails team member before backporting your changes to avoid wasted effort.

While this seems like a feature, I feel like this is a bug, considering that it's taken down high traffic Rails applications. For us it's shot developers in the foot when running migrations that touch a table with a query in a transaction. As a result we've had to do multiple force deploys, but that seems like a workaround as opposed to this PR.

It's a one line change in ApplicationRecord as per the first post to implement this functionality. The new config entry has just made that idiomatic.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants