Skip to content

Fix #400 Add Rails/TransactionRequiresNew cop #414

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changes

* [#410](https://github.com/rubocop-hq/rubocop-rails/pull/410): Add new`Rails/TransactionRequiresNew` cop. ([@the-wendell][])
Copy link
Member

Choose a reason for hiding this comment

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

This entry belongs to the ### New features category. Can you create category and move to it?

* [#409](https://github.com/rubocop-hq/rubocop-rails/pull/409): Deconstruct "table.column" in `Rails/WhereNot`. ([@mobilutz][])

## 2.9.1 (2020-12-16)
Expand Down Expand Up @@ -329,3 +330,4 @@
[@Tietew]: https://github.com/Tietew
[@cilim]: https://github.com/cilim
[@flanger001]: https://github.com/flanger001
[@the-wendell]: https://github.com/the-wendell
6 changes: 5 additions & 1 deletion config/default.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Common configuration.

inherit_mode:
merge:
- Exclude
Expand Down Expand Up @@ -695,6 +694,11 @@ Rails/TimeZone:
- strict
- flexible

Rails/TransactionRequiresNew:
Description: 'When using a transaction requires that the argument `requires_new: true` is passed.'
Copy link
Member

Choose a reason for hiding this comment

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

I think the reference will help users.

Reference: https://makandracards.com/makandra/42885-nested-activerecord-transaction-pitfalls

Enabled: true
Copy link
Member

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, it's not a common rule. I think it should be Enable: false by default.

VersionAdded: '2.10'
Copy link
Member

Choose a reason for hiding this comment

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

I think this auto-correction will be false because the behavior will change.

SafeAutoCorrect: false


Rails/UniqBeforePluck:
Description: 'Prefer the use of uniq or distinct before pluck.'
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ based on the https://rails.rubystyle.guide/[Rails Style Guide].
* xref:cops_rails.adoc#railsskipsmodelvalidations[Rails/SkipsModelValidations]
* xref:cops_rails.adoc#railssquishedsqlheredocs[Rails/SquishedSQLHeredocs]
* xref:cops_rails.adoc#railstimezone[Rails/TimeZone]
* xref:cops_rails.adoc#railstransactionrequiresnew[Rails/TransactionRequiresNew]
* xref:cops_rails.adoc#railsuniqbeforepluck[Rails/UniqBeforePluck]
* xref:cops_rails.adoc#railsuniquevalidationwithoutindex[Rails/UniqueValidationWithoutIndex]
* xref:cops_rails.adoc#railsunknownenv[Rails/UnknownEnv]
Expand Down
31 changes: 31 additions & 0 deletions docs/modules/ROOT/pages/cops_rails.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4202,6 +4202,37 @@ Time.at(timestamp).in_time_zone
* https://rails.rubystyle.guide#time
* http://danilenko.org/2012/7/6/rails_timezones

== Rails/TransactionRequiresNew

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Enabled
| Yes
| Yes
| 2.10
| -
|===



=== Examples

==== Enabled (default)

[source,ruby]
----
# bad
ActiveRecord::Base.transaction do
some_database_stuff
end

# good
ActiveRecord::Base.transaction(requires_new: true) do
Copy link
Member

Choose a reason for hiding this comment

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

Is it okay not to specify joinable: false together?

Copy link
Author

Choose a reason for hiding this comment

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

My understanding is that they work independently. requires_new: true is for custom tranaction (transaction do ... end) and joinable: false is for the transactions created by ActiveRecord methods such as update!

Anyways, I will do some research and testing on this. I have read many blogs/articles about it so far but none of them seem super clear on this topic. I will try to figure it out and come up with some good documentation and examples. I might even write a medium article 😂

some_database_stuff
end
----

== Rails/UniqBeforePluck

|===
Expand Down
54 changes: 54 additions & 0 deletions lib/rubocop/cop/rails/transaction_requires_new.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# @example Enabled (default)
# # bad
# ActiveRecord::Base.transaction do
# some_database_stuff
# end
#
# # good
# ActiveRecord::Base.transaction(requires_new: true) do
# some_database_stuff
# end
#
class TransactionRequiresNew < Base
extend AutoCorrector
MSG = 'Always pass "requires_new: true" to transactions.'

def_node_matcher :transaction?, <<~PATTERN
(block
(send _ :transaction ...)
(args) {_})
PATTERN
Copy link
Member

Choose a reason for hiding this comment

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

Can you replace RESTRICT_ON_SEND = %i[transaction].freeze?


def_node_matcher :requires_new_argument_passed?, <<~PATTERN
(block
(send _ :transaction (hash ... (pair (sym :requires_new) (true) ) ))
(args) {_})
PATTERN

def_node_matcher :arguments_present?, <<~PATTERN
(block
(send _ :transaction _+ )
(args) {_}
)
PATTERN

def on_block(node)
return unless transaction?(node) && !requires_new_argument_passed?(node)

if arguments_present?(node)
add_offense(node)
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it is possible to add_offense whenever it will be auto-correction :-)

else
add_offense(node) do |corrector|
corrector.replace(node, node.source.gsub('.transaction', '.transaction(requires_new: true)'))
Copy link

@stokarenko stokarenko Mar 6, 2021

Choose a reason for hiding this comment

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

transaction do may be called as own Model method and without a dot at the beginning. Cop is catching such calls while auto-correct is running into infinitive loop

Suggested change
corrector.replace(node, node.source.gsub('.transaction', '.transaction(requires_new: true)'))
corrector.replace(node, node.source.gsub('transaction', 'transaction(requires_new: true)'))

Anyway this gsub is wide enough and becomes to be even wider with suggestion above, affecting the whole block including comments, nested method calls like

myvar.transactional_user_actions(foo: :bar) # before

myvar.transaction(requires_new: true)al_user_actions(foo: :bar) # after

etc

end
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
require_relative 'rails/skips_model_validations'
require_relative 'rails/squished_sql_heredocs'
require_relative 'rails/time_zone'
require_relative 'rails/transaction_requires_new'
require_relative 'rails/uniq_before_pluck'
require_relative 'rails/unique_validation_without_index'
require_relative 'rails/unknown_env'
Expand Down
81 changes: 81 additions & 0 deletions spec/rubocop/cop/rails/transaction_requires_new_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::TransactionRequiresNew do
subject(:cop) { described_class.new(config) }

let(:config) { RuboCop::Config.new }

context 'when no arguments are passed' do
it 'registers an offense when using `ActiveRecord::Base#transaction`' do
expect_offense(<<~RUBY)
ActiveRecord::Base.transaction do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Always pass "requires_new: true" to transactions.
end
RUBY

expect_correction(<<~RUBY)
ActiveRecord::Base.transaction(requires_new: true) do
end
RUBY
end

it 'registers an offense when using a Model transaction' do
expect_offense(<<~RUBY)
User.transaction do
^^^^^^^^^^^^^^^^^^^ Always pass "requires_new: true" to transactions.
end
RUBY

expect_correction(<<~RUBY)
User.transaction(requires_new: true) do
end
RUBY
end
end

context 'when arguments other than `requires_new: true` are passsed' do
it 'registers an offense when using `ActiveRecord::Base#transaction`' do
expect_offense(<<~RUBY)
ActiveRecord::Base.transaction(some_option: false) do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Always pass "requires_new: true" to transactions.
end
RUBY

# Does NOT auto-correct when arugments are present
expect_correction(<<~RUBY)
ActiveRecord::Base.transaction(some_option: false) do
Copy link
Member

Choose a reason for hiding this comment

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

I think it can be auto-corrected for this offense.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I suppose it can be. I imagine that it will be safe enough to simply add requires_new: true as the last argument in a transaction.

end
RUBY
end

it 'registers an offense when using a Model transaction' do
expect_offense(<<~RUBY)
User.transaction(some_option: true) do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Always pass "requires_new: true" to transactions.
end
RUBY

# does NOT auto-correct when arguments are present
expect_correction(<<~RUBY)
User.transaction(some_option: true) do
Copy link
Member

Choose a reason for hiding this comment

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

Ditto.

end
RUBY
end
end

context 'when `requires_new: true` is passed' do
it 'does not register an offense when using `ActiveRecord::Base#transaction`' do
expect_no_offenses(<<~RUBY)
ActiveRecord::Base.transaction(requires_new: true) do
end
RUBY
end

it 'does not register an offense when using a Model transaction' do
expect_no_offenses(<<~RUBY)
User.transaction(requires_new: true) do
end
RUBY
end
end
end