Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling of negative elements in enum #40679

Merged
merged 1 commit into from Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,9 @@
* Only warn about negative enums if a positive form that would cause conflicts exists.

Fixes #39065.

*Alex Ghiculescu*

* Add option to run `default_scope` on all queries.

Previously, a `default_scope` would only run on select or insert queries. In some cases, like non-Rails tenant sharding solutions, it may be desirable to run `default_scope` on all queries in order to ensure queries are including a foreign key for the shard (ie `blog_id`).
Expand Down
19 changes: 13 additions & 6 deletions activerecord/lib/active_record/enum.rb
Expand Up @@ -189,6 +189,7 @@ def enum(definitions)

_enum_methods_module.module_eval do
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
value_method_names = []
pairs.each do |label, value|
if enum_prefix == true
prefix = "#{name}_"
Expand All @@ -203,6 +204,7 @@ def enum(definitions)

method_friendly_label = label.to_s.gsub(/\W+/, "_")
value_method_name = "#{prefix}#{method_friendly_label}#{suffix}"
value_method_names << value_method_name
enum_values[label] = value
label = label.to_s

Expand All @@ -217,15 +219,14 @@ def enum(definitions)
# scope :active, -> { where(status: 0) }
# scope :not_active, -> { where.not(status: 0) }
if enum_scopes != false
klass.send(:detect_negative_condition!, value_method_name)

klass.send(:detect_enum_conflict!, name, value_method_name, true)
klass.scope value_method_name, -> { where(attr => value) }

klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
end
end
klass.send(:detect_negative_enum_conditions!, value_method_names) if enum_scopes != false
end
enum_values.freeze
end
Expand Down Expand Up @@ -281,10 +282,16 @@ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Acti
}
end

def detect_negative_condition!(method_name)
if method_name.start_with?("not_") && logger
logger.warn "An enum element in #{self.name} uses the prefix 'not_'." \
" This will cause a conflict with auto generated negative scopes."
def detect_negative_enum_conditions!(method_names)
return unless logger

method_names.select { |m| m.start_with?("not_") }.each do |potential_not|
inverted_form = potential_not.sub("not_", "")
if method_names.include?(inverted_form)
logger.warn "Enum element '#{potential_not}' in #{self.name} uses the prefix 'not_'." \
" This has caused a conflict with auto generated negative scopes." \
" Avoid using enum elements starting with 'not' where the positive form is also an element."
end
end
end
end
Expand Down
81 changes: 77 additions & 4 deletions activerecord/test/cases/enum_test.rb
Expand Up @@ -646,14 +646,18 @@ def self.name; "Book"; end
assert_not_predicate computer, :extendedGold?
end

test "enums with a negative condition log a warning" do
test "enum logs a warning if auto-generated negative scopes would clash with other enum names" do
old_logger = ActiveRecord::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new

ActiveRecord::Base.logger = logger

expected_message = "An enum element in Book uses the prefix 'not_'."\
" This will cause a conflict with auto generated negative scopes."
expected_message_1 = "Enum element 'not_sent' in Book uses the prefix 'not_'."\
" This has caused a conflict with auto generated negative scopes."\
" Avoid using enum elements starting with 'not' where the positive form is also an element."

# this message comes from ActiveRecord::Scoping::Named, but it's worth noting that both occur in this case
expected_message_2 = "Creating scope :not_sent. Overwriting existing method Book.not_sent."

Class.new(ActiveRecord::Base) do
def self.name
Expand All @@ -664,7 +668,76 @@ def self.name
end
end

assert_match(expected_message, logger.logged(:warn).first)
assert_includes(logger.logged(:warn), expected_message_1)
assert_includes(logger.logged(:warn), expected_message_2)
ensure
ActiveRecord::Base.logger = old_logger
end

test "enum logs a warning if auto-generated negative scopes would clash with other enum names regardless of order" do
old_logger = ActiveRecord::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new

ActiveRecord::Base.logger = logger

expected_message_1 = "Enum element 'not_sent' in Book uses the prefix 'not_'."\
" This has caused a conflict with auto generated negative scopes."\
" Avoid using enum elements starting with 'not' where the positive form is also an element."

# this message comes from ActiveRecord::Scoping::Named, but it's worth noting that both occur in this case
expected_message_2 = "Creating scope :not_sent. Overwriting existing method Book.not_sent."

Class.new(ActiveRecord::Base) do
def self.name
"Book"
end
silence_warnings do
enum status: [:not_sent, :sent]
Copy link
Member

Choose a reason for hiding this comment

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

Missing a test with _scopes: false since we should not show the warning if that option is passed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good pickup. Added a new test + this fix.

end
end

assert_includes(logger.logged(:warn), expected_message_1)
assert_includes(logger.logged(:warn), expected_message_2)
ensure
ActiveRecord::Base.logger = old_logger
end

test "enum doesn't log a warning if no clashes detected" do
old_logger = ActiveRecord::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new

ActiveRecord::Base.logger = logger

Class.new(ActiveRecord::Base) do
def self.name
"Book"
end
silence_warnings do
enum status: [:not_sent]
end
end

assert_empty(logger.logged(:warn))
ensure
ActiveRecord::Base.logger = old_logger
end

test "enum doesn't log a warning if opting out of scopes" do
old_logger = ActiveRecord::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new

ActiveRecord::Base.logger = logger

Class.new(ActiveRecord::Base) do
def self.name
"Book"
end
silence_warnings do
enum status: [:not_sent, :sent], _scopes: false
end
end

assert_empty(logger.logged(:warn))
ensure
ActiveRecord::Base.logger = old_logger
end
Expand Down