statesman 13.2.0 → 13.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/tests.yml +96 -36
- data/.tool-versions +1 -0
- data/CHANGELOG.md +13 -0
- data/CONTRIBUTING.md +20 -0
- data/Gemfile +5 -0
- data/README.md +117 -0
- data/Rakefile +32 -1
- data/lib/statesman/adapters/active_record.rb +26 -0
- data/lib/statesman/adapters/active_record_queries.rb +20 -8
- data/lib/statesman/adapters/configure_cached_current_state.rb +58 -0
- data/lib/statesman/adapters/type_safe_active_record_queries.rb +3 -0
- data/lib/statesman/bulk_transition.rb +321 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/statesman.rb +3 -0
- data/spec/spec_helper.rb +9 -2
- data/spec/statesman/adapters/active_record_spec.rb +1 -3
- data/spec/statesman/adapters/configure_cached_current_state_spec.rb +175 -0
- data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +12 -0
- data/spec/statesman/bulk_transition_spec.rb +392 -0
- data/spec/support/active_record.rb +1 -0
- data/spec/support/database_container.rb +41 -0
- metadata +7 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
describe Statesman::BulkTransition, :active_record do
|
|
6
|
+
before do
|
|
7
|
+
# The shared MyActiveRecordModel fixture is wired via the older
|
|
8
|
+
# `include ActiveRecordQueries[...]` form, which doesn't define `.transition_class`.
|
|
9
|
+
# BulkTransition relies on the newer `configure_state_machine` convention (the same
|
|
10
|
+
# one `configure_cached_current_state` requires), so configure it here once.
|
|
11
|
+
unless MyActiveRecordModel.respond_to?(:transition_class)
|
|
12
|
+
MyActiveRecordModel.extend(Statesman::Adapters::TypeSafeActiveRecordQueries)
|
|
13
|
+
MyActiveRecordModel.configure_state_machine(
|
|
14
|
+
transition_class: MyActiveRecordModelTransition, initial_state: :initial,
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless MyActiveRecordModel.respond_to?(:cached_state_column_name)
|
|
19
|
+
MyActiveRecordModel.include(Statesman::Adapters::ConfigureCachedCurrentState)
|
|
20
|
+
MyActiveRecordModel.configure_cached_current_state
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
prepare_model_table
|
|
24
|
+
prepare_transitions_table
|
|
25
|
+
|
|
26
|
+
# Other specs redefine MyActiveRecordModel.transition_class without restoring it, so
|
|
27
|
+
# pin it here to stay independent of spec ordering.
|
|
28
|
+
allow(MyActiveRecordModel).to receive(:transition_class).
|
|
29
|
+
and_return(MyActiveRecordModelTransition)
|
|
30
|
+
|
|
31
|
+
# Add a free business column (not one of AR's "magic" created_on/updated_on/etc.
|
|
32
|
+
# timestamp columns, which insert_all auto-populates regardless of row content) so
|
|
33
|
+
# attributes_to_copy has something meaningful to carry forward in this spec.
|
|
34
|
+
ActiveRecord::Base.connection.execute(
|
|
35
|
+
"ALTER TABLE my_active_record_model_transitions ADD COLUMN extra_data text",
|
|
36
|
+
)
|
|
37
|
+
MyActiveRecordModelTransition.reset_column_information
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
let(:machine_class) { MyStateMachine }
|
|
41
|
+
|
|
42
|
+
# How a non-current row's most_recent is stored: NULL when the column is nullable
|
|
43
|
+
# (needed for the unique index on databases without partial indexes, e.g. MySQL),
|
|
44
|
+
# false otherwise - same as the single-transition path.
|
|
45
|
+
def not_most_recent_value
|
|
46
|
+
MyActiveRecordModelTransition.columns_hash["most_recent"].null ? nil : false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def create_model(initial_state: "initial", extra_data: nil)
|
|
50
|
+
model = MyActiveRecordModel.create!
|
|
51
|
+
model.my_active_record_model_transitions.create!(
|
|
52
|
+
to_state: initial_state, sort_key: 10, most_recent: true, metadata: {},
|
|
53
|
+
extra_data: extra_data
|
|
54
|
+
)
|
|
55
|
+
model
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def last_transition_for(id)
|
|
59
|
+
MyActiveRecordModel.find(id).my_active_record_model_transitions.order(:sort_key).last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "#call!" do
|
|
63
|
+
it "transitions only the parents currently in the from state" do
|
|
64
|
+
in_initial = Array.new(3) { create_model.id }
|
|
65
|
+
in_succeeded = create_model(initial_state: "succeeded").id
|
|
66
|
+
|
|
67
|
+
bulk = described_class.new(
|
|
68
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
69
|
+
from: :initial, to: :succeeded
|
|
70
|
+
)
|
|
71
|
+
transitioned = bulk.call!(in_initial + [in_succeeded], metadata: { "foo" => "bar" })
|
|
72
|
+
|
|
73
|
+
expect(transitioned).to match_array(in_initial)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "raises TransitionConflictError and rolls the whole batch back on a conflicting row" do
|
|
77
|
+
ok = create_model.id
|
|
78
|
+
conflicted = create_model.id
|
|
79
|
+
# An out-of-band row occupying the sort_key the new transition would take.
|
|
80
|
+
MyActiveRecordModelTransition.create!(
|
|
81
|
+
my_active_record_model_id: conflicted, to_state: "failed", sort_key: 20,
|
|
82
|
+
most_recent: not_most_recent_value, metadata: {}
|
|
83
|
+
)
|
|
84
|
+
bulk = described_class.new(
|
|
85
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
86
|
+
from: :initial, to: :succeeded
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect { bulk.call!([ok, conflicted]) }.to raise_error(Statesman::TransitionConflictError)
|
|
90
|
+
|
|
91
|
+
expect(MyActiveRecordModelTransition.where(my_active_record_model_id: [ok, conflicted], most_recent: true).
|
|
92
|
+
pluck(:to_state)).to eq(%w[initial initial])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "marks flipped rows as not current the same way the single path does" do
|
|
96
|
+
ids = Array.new(2) { create_model.id }
|
|
97
|
+
bulk = described_class.new(
|
|
98
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
99
|
+
from: :initial, to: :succeeded
|
|
100
|
+
)
|
|
101
|
+
bulk.call!(ids)
|
|
102
|
+
|
|
103
|
+
old_rows = MyActiveRecordModelTransition.where(my_active_record_model_id: ids, to_state: "initial")
|
|
104
|
+
|
|
105
|
+
expect(old_rows.map(&:most_recent)).to all(eq(not_most_recent_value))
|
|
106
|
+
expect(MyActiveRecordModelTransition.where(my_active_record_model_id: ids, most_recent: true).count).
|
|
107
|
+
to eq(ids.size)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "applies the metadata and most_recent flag to each transitioned parent" do
|
|
111
|
+
in_initial = Array.new(3) { create_model.id }
|
|
112
|
+
bulk = described_class.new(
|
|
113
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
114
|
+
from: :initial, to: :succeeded
|
|
115
|
+
)
|
|
116
|
+
bulk.call!(in_initial, metadata: { "foo" => "bar" })
|
|
117
|
+
|
|
118
|
+
MyActiveRecordModel.where(id: in_initial).each do |model|
|
|
119
|
+
last = model.my_active_record_model_transitions.order(:sort_key).last
|
|
120
|
+
expect(last.to_state).to eq("succeeded")
|
|
121
|
+
expect(last.most_recent).to be(true)
|
|
122
|
+
expect(last.metadata).to eq({ "foo" => "bar" })
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "leaves parents not currently in the from state untouched" do
|
|
127
|
+
in_succeeded = create_model(initial_state: "succeeded").id
|
|
128
|
+
bulk = described_class.new(
|
|
129
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
130
|
+
from: :initial, to: :succeeded
|
|
131
|
+
)
|
|
132
|
+
bulk.call!([in_succeeded], metadata: { "foo" => "bar" })
|
|
133
|
+
|
|
134
|
+
expect(last_transition_for(in_succeeded).sort_key).to eq(10)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "is idempotent - re-running skips parents no longer in the from state" do
|
|
138
|
+
ids = Array.new(2) { create_model.id }
|
|
139
|
+
bulk = described_class.new(
|
|
140
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
141
|
+
from: :initial, to: :succeeded
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
first_run = bulk.call!(ids)
|
|
145
|
+
second_run = bulk.call!(ids)
|
|
146
|
+
|
|
147
|
+
expect(first_run).to match_array(ids)
|
|
148
|
+
expect(second_run).to eq([])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "flips the old most_recent row off and sets sort_key = old + 10" do
|
|
152
|
+
id = create_model.id
|
|
153
|
+
bulk = described_class.new(
|
|
154
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
155
|
+
from: :initial, to: :succeeded
|
|
156
|
+
)
|
|
157
|
+
bulk.call!([id])
|
|
158
|
+
|
|
159
|
+
transitions = MyActiveRecordModel.find(id).
|
|
160
|
+
my_active_record_model_transitions.order(:sort_key)
|
|
161
|
+
expect(transitions.map(&:most_recent)).to eq([not_most_recent_value, true])
|
|
162
|
+
expect(transitions.map(&:sort_key)).to eq([10, 20])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "carries forward only explicitly requested attributes_to_copy" do
|
|
166
|
+
id = create_model(extra_data: "keep-me").id
|
|
167
|
+
bulk = described_class.new(
|
|
168
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
169
|
+
from: :initial, to: :succeeded
|
|
170
|
+
)
|
|
171
|
+
bulk.call!([id], attributes_to_copy: ["extra_data"])
|
|
172
|
+
|
|
173
|
+
expect(last_transition_for(id).extra_data).to eq("keep-me")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "does not carry forward attributes that weren't requested" do
|
|
177
|
+
id = create_model(extra_data: "should-not-persist").id
|
|
178
|
+
bulk = described_class.new(
|
|
179
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
180
|
+
from: :initial, to: :succeeded
|
|
181
|
+
)
|
|
182
|
+
bulk.call!([id]) # no attributes_to_copy
|
|
183
|
+
|
|
184
|
+
expect(last_transition_for(id).extra_data).to be_nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe "flip query RETURNING clause" do
|
|
188
|
+
def flip_sql_for(bulk, id, **options)
|
|
189
|
+
statements = []
|
|
190
|
+
subscriber = ->(*, payload) do
|
|
191
|
+
statements << payload[:sql] if /\AUPDATE\s+"?my_active_record_model_transitions/i.match?(payload[:sql])
|
|
192
|
+
end
|
|
193
|
+
ActiveSupport::Notifications.subscribed(subscriber, "sql.active_record") do
|
|
194
|
+
bulk.call!([id], **options)
|
|
195
|
+
end
|
|
196
|
+
statements.find { |sql| sql.include?(" RETURNING ") }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
let(:bulk) do
|
|
200
|
+
described_class.new(
|
|
201
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
202
|
+
from: :initial, to: :succeeded
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "returns only the carried columns, not *" do
|
|
207
|
+
skip "RETURNING is PostgreSQL-only" unless postgres? # rubocop:disable RSpec/Pending
|
|
208
|
+
id = create_model(extra_data: "x").id
|
|
209
|
+
sql = flip_sql_for(bulk, id, attributes_to_copy: ["extra_data"])
|
|
210
|
+
returning = sql[/RETURNING (.*)\z/m, 1]
|
|
211
|
+
|
|
212
|
+
expect(sql).to_not include("RETURNING *")
|
|
213
|
+
expect(returning.split(", ")).to contain_exactly(
|
|
214
|
+
'"my_active_record_model_id"', '"sort_key"', '"extra_data"'
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "does not return columns that were not requested" do
|
|
219
|
+
skip "RETURNING is PostgreSQL-only" unless postgres? # rubocop:disable RSpec/Pending
|
|
220
|
+
id = create_model(extra_data: "x").id
|
|
221
|
+
sql = flip_sql_for(bulk, id, attributes_to_copy: [])
|
|
222
|
+
|
|
223
|
+
expect(sql[/RETURNING (.*)\z/m, 1]).to_not include("extra_data")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "raises ValidationError for unknown attributes_to_copy columns" do
|
|
228
|
+
id = create_model.id
|
|
229
|
+
bulk = described_class.new(
|
|
230
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
231
|
+
from: :initial, to: :succeeded
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect do
|
|
235
|
+
bulk.call!([id], attributes_to_copy: ["not_a_real_column"])
|
|
236
|
+
end.to raise_error(Statesman::BulkTransition::ValidationError, /not_a_real_column/)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "raises InvalidTransitionError for a from/to pair the machine doesn't allow" do
|
|
240
|
+
expect do
|
|
241
|
+
described_class.new(
|
|
242
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
243
|
+
from: :succeeded, to: :succeeded
|
|
244
|
+
)
|
|
245
|
+
end.to raise_error(Statesman::InvalidTransitionError)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "runs the block inside the transaction and rolls back everything if it raises" do
|
|
249
|
+
id = create_model.id
|
|
250
|
+
bulk = described_class.new(
|
|
251
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
252
|
+
from: :initial, to: :succeeded
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
expect do
|
|
256
|
+
bulk.call!([id]) { |_rows| raise "boom" }
|
|
257
|
+
end.to raise_error("boom")
|
|
258
|
+
|
|
259
|
+
last = last_transition_for(id)
|
|
260
|
+
expect(last.to_state).to eq("initial")
|
|
261
|
+
expect(last.most_recent).to be(true)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "matches per-parent overrides by the string form of the id" do
|
|
265
|
+
id = create_model(extra_data: "from-previous-row").id
|
|
266
|
+
bulk = described_class.new(
|
|
267
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
268
|
+
from: :initial, to: :succeeded
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
bulk.call!(
|
|
272
|
+
[id],
|
|
273
|
+
metadata_per_id: { id.to_s => { "origin" => "api" } },
|
|
274
|
+
attributes_to_copy: ["extra_data"],
|
|
275
|
+
attributes_per_id: { id.to_s => { extra_data: "looked-up-value" } },
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(last_transition_for(id).metadata).to eq({ "origin" => "api" })
|
|
279
|
+
expect(last_transition_for(id).extra_data).to eq("looked-up-value")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "raises ValidationError for attributes_per_id columns not in attributes_to_copy" do
|
|
283
|
+
id = create_model.id
|
|
284
|
+
bulk = described_class.new(
|
|
285
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
286
|
+
from: :initial, to: :succeeded
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
expect do
|
|
290
|
+
bulk.call!([id], attributes_per_id: { id => { "extra_data" => "x" } })
|
|
291
|
+
end.to raise_error(Statesman::BulkTransition::ValidationError, /extra_data/)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "yields a relation over the inserted transitions to the block" do
|
|
295
|
+
id = create_model.id
|
|
296
|
+
bulk = described_class.new(
|
|
297
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
298
|
+
from: :initial, to: :succeeded
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
yielded = nil
|
|
302
|
+
bulk.call!([id]) { |transitions| yielded = transitions.pluck(:my_active_record_model_id, :to_state) }
|
|
303
|
+
|
|
304
|
+
expect(yielded).to eq([[id, "succeeded"]])
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it "invokes a callback object's #call via normal method dispatch, not bare yield" do
|
|
308
|
+
id = create_model.id
|
|
309
|
+
bulk = described_class.new(
|
|
310
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
311
|
+
from: :initial, to: :succeeded
|
|
312
|
+
)
|
|
313
|
+
callback = ->(_rows) {}
|
|
314
|
+
|
|
315
|
+
expect(callback).to receive(:call).once.and_call_original
|
|
316
|
+
|
|
317
|
+
bulk.call!([id], &callback)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it "returns an empty array and does nothing for an empty id list" do
|
|
321
|
+
bulk = described_class.new(
|
|
322
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
323
|
+
from: :initial, to: :succeeded
|
|
324
|
+
)
|
|
325
|
+
expect(bulk.call!([])).to eq([])
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it "overrides metadata per-parent via metadata_per_id, falling back to the uniform metadata" do
|
|
329
|
+
overridden_id = create_model.id
|
|
330
|
+
default_id = create_model.id
|
|
331
|
+
bulk = described_class.new(
|
|
332
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
333
|
+
from: :initial, to: :succeeded
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
bulk.call!(
|
|
337
|
+
[overridden_id, default_id],
|
|
338
|
+
metadata: { "origin" => "gocardless" },
|
|
339
|
+
metadata_per_id: { overridden_id => { "origin" => "api" } },
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
expect(last_transition_for(overridden_id).metadata).to eq({ "origin" => "api" })
|
|
343
|
+
expect(last_transition_for(default_id).metadata).to eq({ "origin" => "gocardless" })
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it "overrides a copied attribute per-parent via attributes_per_id" do
|
|
347
|
+
overridden_id = create_model(extra_data: "from-previous-row").id
|
|
348
|
+
default_id = create_model(extra_data: "from-previous-row").id
|
|
349
|
+
bulk = described_class.new(
|
|
350
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
351
|
+
from: :initial, to: :succeeded
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
bulk.call!(
|
|
355
|
+
[overridden_id, default_id],
|
|
356
|
+
attributes_to_copy: ["extra_data"],
|
|
357
|
+
attributes_per_id: { overridden_id => { "extra_data" => "looked-up-value" } },
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
expect(last_transition_for(overridden_id).extra_data).to eq("looked-up-value")
|
|
361
|
+
expect(last_transition_for(default_id).extra_data).to eq("from-previous-row")
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
it "yields only the new transitions, with their final most_recent state" do
|
|
365
|
+
id = create_model.id
|
|
366
|
+
bulk = described_class.new(
|
|
367
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
368
|
+
from: :initial, to: :succeeded
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
yielded = nil
|
|
372
|
+
bulk.call!([id]) { |transitions| yielded = transitions.pluck(:to_state, :most_recent) }
|
|
373
|
+
|
|
374
|
+
expect(yielded).to eq([["succeeded", true]])
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
describe "cached_current_state integration" do
|
|
379
|
+
it "bulk-updates the cached column for every transitioned parent" do
|
|
380
|
+
ids = Array.new(2) { create_model.id }
|
|
381
|
+
bulk = described_class.new(
|
|
382
|
+
model_class: MyActiveRecordModel, machine_class: machine_class,
|
|
383
|
+
from: :initial, to: :succeeded
|
|
384
|
+
)
|
|
385
|
+
bulk.call!(ids)
|
|
386
|
+
|
|
387
|
+
MyActiveRecordModel.where(id: ids).each do |model|
|
|
388
|
+
expect(model.cached_current_state).to eq("succeeded")
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "testcontainers/mysql"
|
|
4
|
+
require "testcontainers/postgres"
|
|
5
|
+
|
|
6
|
+
# Starts a throwaway database in Docker so the suite can run against PostgreSQL or
|
|
7
|
+
# MySQL locally without a pre-provisioned server.
|
|
8
|
+
module DatabaseContainer
|
|
9
|
+
DEFAULT_IMAGES = {
|
|
10
|
+
"postgres" => "postgres:17",
|
|
11
|
+
"mysql" => "mysql:8.4",
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.start(db, image: nil)
|
|
15
|
+
image ||= DEFAULT_IMAGES.fetch(db) do
|
|
16
|
+
raise ArgumentError, "Unsupported DB '#{db}', expected one of: " \
|
|
17
|
+
"#{DEFAULT_IMAGES.keys.join(', ')}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
container = build(db, image)
|
|
21
|
+
container.start
|
|
22
|
+
at_exit { container.remove(force: true) }
|
|
23
|
+
|
|
24
|
+
database_url(container)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.build(db, image)
|
|
28
|
+
case db
|
|
29
|
+
when "postgres" then Testcontainers::PostgresContainer.new(image)
|
|
30
|
+
when "mysql" then Testcontainers::MysqlContainer.new(image)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.database_url(container)
|
|
35
|
+
if container.is_a?(Testcontainers::MysqlContainer)
|
|
36
|
+
container.database_url(protocol: "mysql2")
|
|
37
|
+
else
|
|
38
|
+
container.database_url
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: statesman
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 13.
|
|
4
|
+
version: 13.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GoCardless
|
|
@@ -25,6 +25,7 @@ files:
|
|
|
25
25
|
- ".rubocop.yml"
|
|
26
26
|
- ".rubocop_todo.yml"
|
|
27
27
|
- ".ruby-version"
|
|
28
|
+
- ".tool-versions"
|
|
28
29
|
- CHANGELOG.md
|
|
29
30
|
- CONTRIBUTING.md
|
|
30
31
|
- Gemfile
|
|
@@ -43,9 +44,11 @@ files:
|
|
|
43
44
|
- lib/statesman/adapters/active_record.rb
|
|
44
45
|
- lib/statesman/adapters/active_record_queries.rb
|
|
45
46
|
- lib/statesman/adapters/active_record_transition.rb
|
|
47
|
+
- lib/statesman/adapters/configure_cached_current_state.rb
|
|
46
48
|
- lib/statesman/adapters/memory.rb
|
|
47
49
|
- lib/statesman/adapters/memory_transition.rb
|
|
48
50
|
- lib/statesman/adapters/type_safe_active_record_queries.rb
|
|
51
|
+
- lib/statesman/bulk_transition.rb
|
|
49
52
|
- lib/statesman/callback.rb
|
|
50
53
|
- lib/statesman/config.rb
|
|
51
54
|
- lib/statesman/exceptions.rb
|
|
@@ -64,10 +67,12 @@ files:
|
|
|
64
67
|
- spec/statesman/adapters/active_record_queries_spec.rb
|
|
65
68
|
- spec/statesman/adapters/active_record_spec.rb
|
|
66
69
|
- spec/statesman/adapters/active_record_transition_spec.rb
|
|
70
|
+
- spec/statesman/adapters/configure_cached_current_state_spec.rb
|
|
67
71
|
- spec/statesman/adapters/memory_spec.rb
|
|
68
72
|
- spec/statesman/adapters/memory_transition_spec.rb
|
|
69
73
|
- spec/statesman/adapters/shared_examples.rb
|
|
70
74
|
- spec/statesman/adapters/type_safe_active_record_queries_spec.rb
|
|
75
|
+
- spec/statesman/bulk_transition_spec.rb
|
|
71
76
|
- spec/statesman/callback_spec.rb
|
|
72
77
|
- spec/statesman/config_spec.rb
|
|
73
78
|
- spec/statesman/exceptions_spec.rb
|
|
@@ -75,6 +80,7 @@ files:
|
|
|
75
80
|
- spec/statesman/machine_spec.rb
|
|
76
81
|
- spec/statesman/utils_spec.rb
|
|
77
82
|
- spec/support/active_record.rb
|
|
83
|
+
- spec/support/database_container.rb
|
|
78
84
|
- spec/support/exactly_query_databases.rb
|
|
79
85
|
- spec/support/generators_shared_examples.rb
|
|
80
86
|
- statesman.gemspec
|