Add setting for enumerating column names in SELECT statements

This commit is contained in:
Matt Duszynski 2021-03-21 15:17:22 -07:00
parent 9e1cdf27eb
commit 726abeaab4
5 changed files with 61 additions and 1 deletions

@ -1,3 +1,43 @@
* Add setting for enumerating column names in SELECT statements.
Adding a column to a Postgres database while the application is running can
change the result of wildcard `SELECT *` queries, which invalidates the result
of cached prepared statements and raises a PreparedStatementCacheExpired error.
When truthy, ActiveRecord will avoid wildcards and always include column names
in `SELECT` queries, which will return consistent results and avoid prepared
statement errors.
Before:
```ruby
Book.limit(5)
# SELECT * FROM books LIMIT 5
```
After:
```ruby
# config/application.rb
module MyApp
class Application < Rails::Application
config.active_record.enumerate_columns_in_select_statements = true
end
end
# or, configure per-model
class Book < ApplicationRecord
self.enumerate_columns_in_select_statements = true
end
```
```ruby
Book.limit(5)
# SELECT id, author_id, name, format, status, language, etc FROM books LIMIT 5
```
*Matt Duszynski*
* Only update dirty attributes once for cyclic autosave callbacks.
Instead of calling `changes_applied` everytime `save` is called,

@ -129,6 +129,14 @@ def self.configurations
# for multiple databases.
mattr_accessor :suppress_multiple_database_warning, instance_writer: false, default: false
##
# :singleton-method:
# Force enumeration of all columns in SELECT statements
# e.g. `SELECT first_name, last_name FROM ...` instead of `SELECT * FROM ...`
# This avoids PreparedStatementCacheExpired errors when a column is added
# to a Postgres database while the app is running.
class_attribute :enumerate_columns_in_select_statements, instance_accessor: false, default: false
mattr_accessor :maintain_test_schema, instance_accessor: false
class_attribute :belongs_to_required_by_default, instance_accessor: false

@ -1390,7 +1390,7 @@ def build_joins(join_sources, aliases = nil)
def build_select(arel)
if select_values.any?
arel.project(*arel_columns(select_values))
elsif klass.ignored_columns.any?
elsif klass.ignored_columns.any? || klass.enumerate_columns_in_select_statements
arel.project(*klass.column_names.map { |field| table[field] })
else
arel.project(table[Arel.star])

@ -77,5 +77,15 @@ def test_star_select_with_joins_and_includes
tags_count indestructible_tags_count tags_with_destroy_count tags_with_nullify_count
), posts.first.attributes.keys
end
def test_enumerate_columns_in_select_statements
original_value = Post.enumerate_columns_in_select_statements
Post.enumerate_columns_in_select_statements = true
sql = Post.all.to_sql
Post.column_names.each do |column_name|
assert_includes sql, column_name
end
Post.enumerate_columns_in_select_statements = original_value
end
end
end

@ -473,6 +473,8 @@ in controllers and views. This defaults to `false`.
* `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`.
The MySQL adapter adds one additional configuration option:
* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`.