Skip to content

Commit 2a9d8ac

Browse files
Fixed issue with transitions using STI (#503)
Prior to 7.2.0 the code to unset the most_recent column of old transitions when a new transition was created relied on transitions_for_parent, which would build the correct query in the case that the transition class was using single table inheritance. However, after #399, the code no longer uses the parent association to create the query and the current custom query lacks this functionality that existed prior to 7.2.0. This PR addresses this issue by adding an AND clause to the query to filter by transition type if the transition class contains an inheritance column.
1 parent 3101b07 commit 2a9d8ac

File tree

4 files changed

+180
-8
lines changed

4 files changed

+180
-8
lines changed

lib/statesman/adapters/active_record.rb

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,24 @@ def update_most_recents(most_recent_id = nil)
158158

159159
def most_recent_transitions(most_recent_id = nil)
160160
if most_recent_id
161-
transitions_of_parent.and(
161+
concrete_transitions_of_parent.and(
162162
transition_table[:id].eq(most_recent_id).or(
163163
transition_table[:most_recent].eq(true),
164164
),
165165
)
166166
else
167-
transitions_of_parent.and(transition_table[:most_recent].eq(true))
167+
concrete_transitions_of_parent.and(transition_table[:most_recent].eq(true))
168+
end
169+
end
170+
171+
def concrete_transitions_of_parent
172+
if transition_sti?
173+
transitions_of_parent.and(
174+
transition_table[transition_class.inheritance_column].
175+
eq(transition_class.name),
176+
)
177+
else
178+
transitions_of_parent
168179
end
169180
end
170181

@@ -263,13 +274,18 @@ def unique_indexes
263274
end
264275
end
265276

266-
def parent_join_foreign_key
267-
association =
268-
parent_model.class.
269-
reflect_on_all_associations(:has_many).
270-
find { |r| r.name.to_s == @association_name.to_s }
277+
def transition_sti?
278+
transition_class.column_names.include?(transition_class.inheritance_column)
279+
end
271280

272-
association_join_primary_key(association)
281+
def parent_association
282+
parent_model.class.
283+
reflect_on_all_associations(:has_many).
284+
find { |r| r.name.to_s == @association_name.to_s }
285+
end
286+
287+
def parent_join_foreign_key
288+
association_join_primary_key(parent_association)
273289
end
274290

275291
def association_join_primary_key(association)

spec/spec_helper.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def connection_failure
4848
my_namespace_my_active_record_model_transitions
4949
other_active_record_models
5050
other_active_record_model_transitions
51+
sti_active_record_models
52+
sti_active_record_model_transitions
5153
]
5254
tables.each do |table_name|
5355
sql = "DROP TABLE IF EXISTS #{table_name};"
@@ -72,6 +74,15 @@ def prepare_other_transitions_table
7274
OtherActiveRecordModelTransition.reset_column_information
7375
end
7476

77+
def prepare_sti_model_table
78+
CreateStiActiveRecordModelMigration.migrate(:up)
79+
end
80+
81+
def prepare_sti_transitions_table
82+
CreateStiActiveRecordModelTransitionMigration.migrate(:up)
83+
StiActiveRecordModelTransition.reset_column_information
84+
end
85+
7586
MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON)
7687
end
7788
end

spec/statesman/adapters/active_record_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
MyActiveRecordModelTransition.serialize(:metadata, JSON)
1414

15+
prepare_sti_model_table
16+
prepare_sti_transitions_table
17+
1518
Statesman.configure do
1619
# Rubocop requires described_class to be used, but this block
1720
# is instance_eval'd and described_class won't be defined
@@ -300,6 +303,57 @@
300303
from(true).to be_falsey
301304
end
302305
end
306+
307+
context "when transition uses STI" do
308+
let(:sti_model) { StiActiveRecordModel.create }
309+
310+
let(:adapter_a) do
311+
described_class.new(
312+
StiAActiveRecordModelTransition,
313+
sti_model,
314+
observer,
315+
{ association_name: :sti_a_active_record_model_transitions },
316+
)
317+
end
318+
let(:adapter_b) do
319+
described_class.new(
320+
StiBActiveRecordModelTransition,
321+
sti_model,
322+
observer,
323+
{ association_name: :sti_b_active_record_model_transitions },
324+
)
325+
end
326+
let(:create) { adapter_a.create(from, to) }
327+
328+
context "with a previous unrelated transition" do
329+
let!(:transition_b) { adapter_b.create(from, to) }
330+
331+
its(:most_recent) { is_expected.to eq(true) }
332+
333+
it "doesn't update the previous transition's most_recent flag" do
334+
expect { create }.
335+
to_not(change { transition_b.reload.most_recent })
336+
end
337+
end
338+
339+
context "with previous related and unrelated transitions" do
340+
let!(:transition_a) { adapter_a.create(from, to) }
341+
let!(:transition_b) { adapter_b.create(from, to) }
342+
343+
its(:most_recent) { is_expected.to eq(true) }
344+
345+
it "updates the previous transition's most_recent flag" do
346+
expect { create }.
347+
to change { transition_a.reload.most_recent }.
348+
from(true).to be_falsey
349+
end
350+
351+
it "doesn't update the previous unrelated transition's most_recent flag" do
352+
expect { create }.
353+
to_not(change { transition_b.reload.most_recent })
354+
end
355+
end
356+
end
303357
end
304358
end
305359

spec/support/active_record.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,94 @@ def change
278278
end
279279
end
280280
end
281+
282+
class StiActiveRecordModel < ActiveRecord::Base
283+
has_many :sti_a_active_record_model_transitions, autosave: false
284+
has_many :sti_b_active_record_model_transitions, autosave: false
285+
286+
def state_machine_a
287+
@state_machine_a ||= MyStateMachine.new(
288+
self, transition_class: StiAActiveRecordModelTransition
289+
)
290+
end
291+
292+
def state_machine_b
293+
@state_machine_b ||= MyStateMachine.new(
294+
self, transition_class: StiBActiveRecordModelTransition
295+
)
296+
end
297+
298+
def metadata
299+
super || {}
300+
end
301+
302+
def reload(*)
303+
state_machine_a.reset
304+
state_machine_b.reset
305+
super
306+
end
307+
end
308+
309+
class StiActiveRecordModelTransition < ActiveRecord::Base
310+
include Statesman::Adapters::ActiveRecordTransition
311+
312+
belongs_to :sti_active_record_model
313+
serialize :metadata, JSON
314+
end
315+
316+
class StiAActiveRecordModelTransition < StiActiveRecordModelTransition
317+
end
318+
319+
class StiBActiveRecordModelTransition < StiActiveRecordModelTransition
320+
end
321+
322+
class CreateStiActiveRecordModelMigration < MIGRATION_CLASS
323+
def change
324+
create_table :sti_active_record_models do |t|
325+
t.timestamps null: false
326+
end
327+
end
328+
end
329+
330+
class CreateStiActiveRecordModelTransitionMigration < MIGRATION_CLASS
331+
def change
332+
create_table :sti_active_record_model_transitions do |t|
333+
t.string :to_state
334+
t.integer :sti_active_record_model_id
335+
t.integer :sort_key
336+
t.string :type
337+
338+
# MySQL doesn't allow default values on text fields
339+
if ActiveRecord::Base.connection.adapter_name == "Mysql2"
340+
t.text :metadata
341+
else
342+
t.text :metadata, default: "{}"
343+
end
344+
345+
if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
346+
t.boolean :most_recent, default: true, null: false
347+
else
348+
t.boolean :most_recent, default: true
349+
end
350+
351+
t.timestamps null: false
352+
end
353+
354+
add_index :sti_active_record_model_transitions,
355+
%i[type sti_active_record_model_id sort_key],
356+
unique: true, name: "sti_sort_key_index"
357+
358+
if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
359+
add_index :sti_active_record_model_transitions,
360+
%i[type sti_active_record_model_id most_recent],
361+
unique: true,
362+
where: "most_recent",
363+
name: "index_sti_active_record_model_transitions_parent_latest"
364+
else
365+
add_index :sti_active_record_model_transitions,
366+
%i[type sti_active_record_model_id most_recent],
367+
unique: true,
368+
name: "index_sti_active_record_model_transitions_parent_latest"
369+
end
370+
end
371+
end

0 commit comments

Comments
 (0)