From dc318567330208592e487cd01fa45077915d0492 Mon Sep 17 00:00:00 2001 From: Arthur Neves Date: Wed, 12 Jun 2019 16:03:58 -0400 Subject: [PATCH 01/10] Inverse the order of writes on statesman transitions StatesMan will update the old transitions to `most_recent: nil` and then insert the new transition as `most_recent: true`. However that can cause deadlocks on MySQL when running concurrent state transitions. On the update, MySQL will hold a gap lock to the unique indexes there are, so other transactions cannot insert. After two updates on the most_recent and two inserts, the second insert will fail with a deadlock. For more explanation see the PR description that includes this commit. --- lib/statesman/adapters/active_record.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index edf80823..bfaaf41a 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -68,14 +68,16 @@ def create_transition(from, to, metadata) sort_key: next_sort_key, metadata: metadata } - transition_attributes[:most_recent] = true - transition = transitions_for_parent.build(transition_attributes) ::ActiveRecord::Base.transaction(requires_new: true) do @observer.execute(:before, from, to, transition) - unset_old_most_recent + # We save the transition first, and mark it as + # most_recent after to avoid letting MySQL put a + # next-key lock which could cause deadlocks. transition.save! + unset_old_most_recent + transition.update!(most_recent: true) @last_transition = transition @observer.execute(:after, from, to, transition) add_after_commit_callback(from, to, transition) From b4015ae8f13b92defd30640ae7fa1cf56b7f7c6d Mon Sep 17 00:00:00 2001 From: Arthur Neves Date: Wed, 12 Jun 2019 16:54:26 -0400 Subject: [PATCH 02/10] Make sure the initial most_recent state is false/nil --- lib/statesman/adapters/active_record.rb | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index bfaaf41a..4280e01f 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -64,11 +64,9 @@ def last(force_reload: false) private def create_transition(from, to, metadata) - transition_attributes = { to_state: to, - sort_key: next_sort_key, - metadata: metadata } - - transition = transitions_for_parent.build(transition_attributes) + transition = transitions_for_parent.build( + default_transition_attributes(to, metadata), + ) ::ActiveRecord::Base.transaction(requires_new: true) do @observer.execute(:before, from, to, transition) @@ -86,6 +84,20 @@ def create_transition(from, to, metadata) transition end + def default_transition_attributes(to, metadata) + transition_attributes = { to_state: to, + sort_key: next_sort_key, + metadata: metadata } + + # see comment on `unset_old_most_recent` method + if transition_class.columns_hash["most_recent"].null == false + transition_attributes[:most_recent] = false + else + transition_attributes[:most_recent] = nil + end + transition_attributes + end + def add_after_commit_callback(from, to, transition) ::ActiveRecord::Base.connection.add_transaction_record( ActiveRecordAfterCommitWrap.new do From a569a7c62fe69e565709e3d1db2d83c0b7fe93a8 Mon Sep 17 00:00:00 2001 From: Lawrence Jones Date: Thu, 13 Jun 2019 12:25:15 +0100 Subject: [PATCH 03/10] Lock-lesser transitions without additional queries https://github.com/gocardless/statesman/pull/350 @arthurnn opened #350 to reduce the impact of Statesman taking gap locks when using MySQL. The crux of the issue is that MySQL's implementation of REPEATABLE READ can take wide locks when an update touches no rows, which happens frequently on the first transition of Statesman. By first creating the new transition, we can avoid issuing an update that will take the large gap lock. This order of queries meant we added an additional query to the transition step which could impact people who rely on low-latency Statesman transitions. This commit is another take on the same approach that collapses two queries into one, by taking the update of the old and new transition's most_recent column and updating them together. It's slightly janky but if robust, would be a good alternative to avoid additional latency. --- lib/statesman/adapters/active_record.rb | 99 ++++++++++++++++++------- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index 4280e01f..d16e000d 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -63,6 +63,7 @@ def last(force_reload: false) private + # rubocop:disable Metrics/MethodLength def create_transition(from, to, metadata) transition = transitions_for_parent.build( default_transition_attributes(to, metadata), @@ -70,12 +71,20 @@ def create_transition(from, to, metadata) ::ActiveRecord::Base.transaction(requires_new: true) do @observer.execute(:before, from, to, transition) - # We save the transition first, and mark it as - # most_recent after to avoid letting MySQL put a - # next-key lock which could cause deadlocks. + + # We save the transition first with most_recent falsy, then mark most_recent + # true after to avoid letting MySQL acquire a next-key lock which can cause + # deadlocks. + # + # To avoid an additional query, we manually adjust the most_recent attribute on + # our transition assuming that update_most_recents will have set it to true. transition.save! - unset_old_most_recent - transition.update!(most_recent: true) + unless update_most_recents(transition.id) > 0 + raise ActiveRecord::Rollback, "failed to update most_recent" + end + + transition.assign_attributes(most_recent: true) + @last_transition = transition @observer.execute(:after, from, to, transition) add_after_commit_callback(from, to, transition) @@ -83,6 +92,7 @@ def create_transition(from, to, metadata) transition end + # rubocop:enable Metrics/MethodLength def default_transition_attributes(to, metadata) transition_attributes = { to_state: to, @@ -110,21 +120,58 @@ def transitions_for_parent parent_model.send(@association_name) end - def unset_old_most_recent - most_recent = transitions_for_parent.where(most_recent: true) + # Sets the given transition most_recent = t while unsetting the most_recent of any + # previous transitions. + def update_most_recents(most_recent_id) + transitions = transitions_for_parent + last_or_current = transitions.where(id: most_recent_id).or( + transitions.where(most_recent: true) + ) - # Check whether the `most_recent` column allows null values. If it - # doesn't, set old records to `false`, otherwise, set them to `NULL`. - # - # Some conditioning here is required to support databases that don't - # support partial indexes. By doing the conditioning on the column, - # rather than Rails' opinion of whether the database supports partial - # indexes, we're robust to DBs later adding support for partial indexes. - if transition_class.columns_hash["most_recent"].null == false - most_recent.update_all(with_updated_timestamp(most_recent: false)) - else - most_recent.update_all(with_updated_timestamp(most_recent: nil)) + last_or_current.update_all( + build_most_recents_update_all(most_recent_id), + ) + end + + # Generates update_all parameters that will touch the updated timestamp (if valid + # for this model) and ensure only the transition with the most_recent_id has + # most_recent set to true. + # + # This is quite nasty, but combines two updates (set all most_recent = f, set + # current most_recent = t) into one, which helps improve transition performance + # especially when database latency is significant. + # + # The SQL this can help produce looks like: + # + # update transitions + # set most_recent = (case when id = 'PA123' then TRUE else FALSE end) + # , updated_at = '...' + # ... + # + def build_most_recents_update_all(most_recent_id) + clause = "most_recent = (case when id = ? then ? else ? end)" + parameters = [most_recent_id, true, not_most_recent_value] + + updated_column, updated_at = updated_timestamp + if updated_column + clause += ", #{updated_column} = ?" + parameters.push(updated_at) end + + [clause, *parameters] + end + + # Check whether the `most_recent` column allows null values. If it doesn't, set old + # records to `false`, otherwise, set them to `NULL`. + # + # Some conditioning here is required to support databases that don't support partial + # indexes. By doing the conditioning on the column, rather than Rails' opinion of + # whether the database supports partial indexes, we're robust to DBs later adding + # support for partial indexes. + def not_most_recent_value + return false if transition_class.columns_hash["most_recent"].null == false + + nil end def next_sort_key @@ -182,7 +229,8 @@ def association_join_primary_key(association) end end - def with_updated_timestamp(params) + # updated_timestamp should return [column_name, value] + def updated_timestamp # TODO: Once we've set expectations that transition classes should conform to # the interface of Adapters::ActiveRecordTransition as a breaking change in the # next major version, we can stop calling `#respond_to?` first and instead @@ -196,15 +244,12 @@ def with_updated_timestamp(params) ActiveRecordTransition::DEFAULT_UPDATED_TIMESTAMP_COLUMN end - return params if column.nil? - - timestamp = if ::ActiveRecord::Base.default_timezone == :utc - Time.now.utc - else - Time.now - end + # No updated timestamp column, don't return anything + return nil if column.nil? - params.merge(column => timestamp) + [ + column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now, + ] end end From 3c2f075f651ad1ee34ba8ea6c2e4d3e8bbc1b38e Mon Sep 17 00:00:00 2001 From: Arthur Neves Date: Thu, 13 Jun 2019 12:40:09 -0400 Subject: [PATCH 04/10] fmt --- lib/statesman/adapters/active_record.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index d16e000d..2f291d77 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -125,7 +125,7 @@ def transitions_for_parent def update_most_recents(most_recent_id) transitions = transitions_for_parent last_or_current = transitions.where(id: most_recent_id).or( - transitions.where(most_recent: true) + transitions.where(most_recent: true), ) last_or_current.update_all( @@ -248,7 +248,7 @@ def updated_timestamp return nil if column.nil? [ - column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now, + column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now ] end end From 879a16eb5834f271ef9c6cfe6f17c8f1cead50f4 Mon Sep 17 00:00:00 2001 From: Grey Baker Date: Wed, 3 Jul 2019 11:44:07 +0100 Subject: [PATCH 05/10] Replace Arel#or with raw SQL --- lib/statesman/adapters/active_record.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index 2f291d77..993794a7 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -124,8 +124,12 @@ def transitions_for_parent # previous transitions. def update_most_recents(most_recent_id) transitions = transitions_for_parent - last_or_current = transitions.where(id: most_recent_id).or( - transitions.where(most_recent: true), + + # TODO: Once support for Rails 4 is dropped this can be replaced with + # transitions.where(id: most_recent_id).or(transitions.where(most_recent: true) + last_or_current = transitions.where( + "#{transition_class.table_name}.id = #{most_recent_id} "\ + "OR #{transition_class.table_name}.most_recent = #{db_true}", ) last_or_current.update_all( @@ -251,6 +255,10 @@ def updated_timestamp column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now ] end + + def db_true + ::ActiveRecord::Base.connection.quote(true) + end end class ActiveRecordAfterCommitWrap From 16a3b95ff97a5d4b462acf16fd7e3ad67584864e Mon Sep 17 00:00:00 2001 From: Grey Baker Date: Wed, 3 Jul 2019 12:49:11 +0100 Subject: [PATCH 06/10] Attempt to fix typecasting --- lib/statesman/adapters/active_record.rb | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index 993794a7..ea9080c5 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -129,7 +129,8 @@ def update_most_recents(most_recent_id) # transitions.where(id: most_recent_id).or(transitions.where(most_recent: true) last_or_current = transitions.where( "#{transition_class.table_name}.id = #{most_recent_id} "\ - "OR #{transition_class.table_name}.most_recent = #{db_true}", + "OR #{transition_class.table_name}.most_recent = "\ + "#{::ActiveRecord::Base.connection.quote(true)}", ) last_or_current.update_all( @@ -153,8 +154,9 @@ def update_most_recents(most_recent_id) # ... # def build_most_recents_update_all(most_recent_id) - clause = "most_recent = (case when id = ? then ? else ? end)" - parameters = [most_recent_id, true, not_most_recent_value] + clause = "most_recent = "\ + "(case when id = ? then #{db_true} else #{not_most_recent_value} end)" + parameters = [most_recent_id] updated_column, updated_at = updated_timestamp if updated_column @@ -173,9 +175,9 @@ def build_most_recents_update_all(most_recent_id) # whether the database supports partial indexes, we're robust to DBs later adding # support for partial indexes. def not_most_recent_value - return false if transition_class.columns_hash["most_recent"].null == false + return db_false if transition_class.columns_hash["most_recent"].null == false - nil + "NULL" end def next_sort_key @@ -257,7 +259,19 @@ def updated_timestamp end def db_true - ::ActiveRecord::Base.connection.quote(true) + value = ::ActiveRecord::Base.connection.type_cast( + true, + transition_class.columns_hash["most_recent"], + ) + ::ActiveRecord::Base.connection.quote(value) + end + + def db_false + value = ::ActiveRecord::Base.connection.type_cast( + false, + transition_class.columns_hash["most_recent"], + ) + ::ActiveRecord::Base.connection.quote(value) end end From 05b2b3b3a927b774d1677cccd84efb75cffe3afe Mon Sep 17 00:00:00 2001 From: Lawrence Jones Date: Tue, 9 Jul 2019 14:44:16 +0100 Subject: [PATCH 07/10] Fix MySQL issue with ordered updates This commit first refactors the construction of transition SQL statements to use Arel. This is safer and hopefully more readable than constructing large SQL statements using strings. It also fixes a bug with transition updates where MySQL would throw index violations. This was caused by MySQL validating index constraints across a partially applied update, where Statesman would set the latest transition's most_recent = 't' before unsetting the previous. --- lib/statesman/adapters/active_record.rb | 76 ++++++++++++++++--------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index ea9080c5..a3b92e73 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -3,9 +3,6 @@ module Statesman module Adapters class ActiveRecord - attr_reader :transition_class - attr_reader :parent_model - JSON_COLUMN_TYPES = %w[json jsonb].freeze def self.database_supports_partial_indexes? @@ -27,12 +24,15 @@ def initialize(transition_class, parent_model, observer, options = {}) end @transition_class = transition_class + @transition_table = transition_class.arel_table @parent_model = parent_model @observer = observer @association_name = options[:association_name] || @transition_class.table_name end + attr_reader :transition_class, :transition_table, :parent_model + def create(from, to, metadata = {}) create_transition(from.to_s, to.to_s, metadata) rescue ::ActiveRecord::RecordNotUnique => e @@ -79,6 +79,7 @@ def create_transition(from, to, metadata) # To avoid an additional query, we manually adjust the most_recent attribute on # our transition assuming that update_most_recents will have set it to true. transition.save! + unless update_most_recents(transition.id) > 0 raise ActiveRecord::Rollback, "failed to update most_recent" end @@ -122,25 +123,31 @@ def transitions_for_parent # Sets the given transition most_recent = t while unsetting the most_recent of any # previous transitions. - def update_most_recents(most_recent_id) - transitions = transitions_for_parent - - # TODO: Once support for Rails 4 is dropped this can be replaced with - # transitions.where(id: most_recent_id).or(transitions.where(most_recent: true) - last_or_current = transitions.where( - "#{transition_class.table_name}.id = #{most_recent_id} "\ - "OR #{transition_class.table_name}.most_recent = "\ - "#{::ActiveRecord::Base.connection.quote(true)}", + def update_most_recents(most_recent_id) # rubocop:disable Metrics/AbcSize + update = Arel::UpdateManager.new + update.table(transition_table) + + update.where( + transition_table[parent_join_foreign_key.to_sym].eq(parent_model.id).and( + transition_table[:id].eq(most_recent_id).or( + transition_table[:most_recent].eq(true), + ), + ), ) - last_or_current.update_all( - build_most_recents_update_all(most_recent_id), - ) + update.set(build_most_recents_update_all_values(most_recent_id)) + + # MySQL will validate index constraints across the intermediate result of an + # update. This means we must order our update to deactivate the previous + # most_recent before setting the new row to be true. + update.order(transition_table[:most_recent].desc) if db_mysql? + + ::ActiveRecord::Base.connection.exec_update(update.to_sql) end - # Generates update_all parameters that will touch the updated timestamp (if valid - # for this model) and ensure only the transition with the most_recent_id has - # most_recent set to true. + # Generates update_all Arel values that will touch the updated timestamp (if valid + # for this model) and set most_recent to true only for the transition with a + # matching most_recent ID. # # This is quite nasty, but combines two updates (set all most_recent = f, set # current most_recent = t) into one, which helps improve transition performance @@ -153,19 +160,32 @@ def update_most_recents(most_recent_id) # , updated_at = '...' # ... # - def build_most_recents_update_all(most_recent_id) - clause = "most_recent = "\ - "(case when id = ? then #{db_true} else #{not_most_recent_value} end)" - parameters = [most_recent_id] + # rubocop:disable Metrics/MethodLength + def build_most_recents_update_all_values(most_recent_id) + values = [ + [ + transition_table[:most_recent], + Arel::Nodes::SqlLiteral.new( + Arel::Nodes::Case.new. + when(transition_table[:id].eq(most_recent_id)).then(true). + else(not_most_recent_value).to_sql, + ), + ], + ] + # Only if we support the updated at timestamps should we add this column to the + # update updated_column, updated_at = updated_timestamp if updated_column - clause += ", #{updated_column} = ?" - parameters.push(updated_at) + values << [ + transition_table[updated_column.to_sym], + updated_at, + ] end - [clause, *parameters] + values end + # rubocop:enable Metrics/MethodLength # Check whether the `most_recent` column allows null values. If it doesn't, set old # records to `false`, otherwise, set them to `NULL`. @@ -177,7 +197,7 @@ def build_most_recents_update_all(most_recent_id) def not_most_recent_value return db_false if transition_class.columns_hash["most_recent"].null == false - "NULL" + nil end def next_sort_key @@ -258,6 +278,10 @@ def updated_timestamp ] end + def db_mysql? + ::ActiveRecord::Base.connection.adapter_name.downcase.starts_with?("mysql") + end + def db_true value = ::ActiveRecord::Base.connection.type_cast( true, From a316c209190ef974b163704561e38fa5ba58e04f Mon Sep 17 00:00:00 2001 From: Lawrence Jones Date: Tue, 9 Jul 2019 16:48:41 +0100 Subject: [PATCH 08/10] Smooth-over breaking API change in Arel https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f When merged with Rails core, Arel removed an initialisation parameter that was unused in all but a few of the Arel features. For now, provide a helper local to the ActiveRecord adapter that can construct Manager classes independent of the API change. --- lib/statesman/adapters/active_record.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index a3b92e73..c061f913 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -124,7 +124,7 @@ def transitions_for_parent # Sets the given transition most_recent = t while unsetting the most_recent of any # previous transitions. def update_most_recents(most_recent_id) # rubocop:disable Metrics/AbcSize - update = Arel::UpdateManager.new + update = build_arel_manager(::Arel::UpdateManager) update.table(transition_table) update.where( @@ -187,6 +187,18 @@ def build_most_recents_update_all_values(most_recent_id) end # rubocop:enable Metrics/MethodLength + # Provide a wrapper for constructing an update manager which handles a breaking API + # change in Arel as we move into Rails >6.0. + # + # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f + def build_arel_manager(manager) + if manager.instance_method(:initialize).arity.zero? + manager.new + else + manager.new(::ActiveRecord::Base) + end + end + # Check whether the `most_recent` column allows null values. If it doesn't, set old # records to `false`, otherwise, set them to `NULL`. # From 45b27d4acd2f14a51d371859bff71a0e67fe6a76 Mon Sep 17 00:00:00 2001 From: Lawrence Jones Date: Tue, 9 Jul 2019 16:54:49 +0100 Subject: [PATCH 09/10] Rely on Arel to properly quote values When constructing Arel nodes, we don't need to be quoting the values. This happens automatically via the Arel#to_sql interface, so quoting them and passing them into Arel causes them to be quoted twice. --- lib/statesman/adapters/active_record.rb | 27 +++++++------------------ 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index c061f913..b63d0f61 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -160,14 +160,15 @@ def update_most_recents(most_recent_id) # rubocop:disable Metrics/AbcSize # , updated_at = '...' # ... # - # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def build_most_recents_update_all_values(most_recent_id) values = [ [ transition_table[:most_recent], Arel::Nodes::SqlLiteral.new( Arel::Nodes::Case.new. - when(transition_table[:id].eq(most_recent_id)).then(true). + when(transition_table[:id].eq(most_recent_id)). + then(Arel::Nodes::True.new). else(not_most_recent_value).to_sql, ), ], @@ -185,7 +186,7 @@ def build_most_recents_update_all_values(most_recent_id) values end - # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # Provide a wrapper for constructing an update manager which handles a breaking API # change in Arel as we move into Rails >6.0. @@ -207,7 +208,9 @@ def build_arel_manager(manager) # whether the database supports partial indexes, we're robust to DBs later adding # support for partial indexes. def not_most_recent_value - return db_false if transition_class.columns_hash["most_recent"].null == false + if transition_class.columns_hash["most_recent"].null == false + return Arel::Nodes::False.new + end nil end @@ -293,22 +296,6 @@ def updated_timestamp def db_mysql? ::ActiveRecord::Base.connection.adapter_name.downcase.starts_with?("mysql") end - - def db_true - value = ::ActiveRecord::Base.connection.type_cast( - true, - transition_class.columns_hash["most_recent"], - ) - ::ActiveRecord::Base.connection.quote(value) - end - - def db_false - value = ::ActiveRecord::Base.connection.type_cast( - false, - transition_class.columns_hash["most_recent"], - ) - ::ActiveRecord::Base.connection.quote(value) - end end class ActiveRecordAfterCommitWrap From 897aacd0b1ba3798a7d805c0fb47831566df057f Mon Sep 17 00:00:00 2001 From: Lawrence Jones Date: Tue, 9 Jul 2019 17:15:33 +0100 Subject: [PATCH 10/10] Handle older versions of ActiveRecord conn Where the parameters of exec_update were not defaulted to sensible values, and must be supplied. --- lib/statesman/adapters/active_record.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/statesman/adapters/active_record.rb b/lib/statesman/adapters/active_record.rb index b63d0f61..5d21e24c 100644 --- a/lib/statesman/adapters/active_record.rb +++ b/lib/statesman/adapters/active_record.rb @@ -142,7 +142,7 @@ def update_most_recents(most_recent_id) # rubocop:disable Metrics/AbcSize # most_recent before setting the new row to be true. update.order(transition_table[:most_recent].desc) if db_mysql? - ::ActiveRecord::Base.connection.exec_update(update.to_sql) + ::ActiveRecord::Base.connection.exec_update(update.to_sql, nil, []) end # Generates update_all Arel values that will touch the updated timestamp (if valid