statesman 11.0.0 → 12.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd332665b3cb66bb2d454e378a7e591e03c06ca4ec8fa36e2f9b69ada1889af8
4
- data.tar.gz: cdd845b1804643b723f9eb328cbeaa773797fac3f1caf2b4cb5f758d266746a2
3
+ metadata.gz: a799d928f8e674203dde325b7653ed8fe2b719f5b9108ded9353afaa5eefe028
4
+ data.tar.gz: a92e8f0a4a49a22e66c4d3a83ccdec62e042cf8cd2a734368090f9144373f02a
5
5
  SHA512:
6
- metadata.gz: b5bc3b2f8eaf809edc0537f28291f420113ad8d538e79557fb3f7f84e54dc978ff9774be4a9af3a16d2de9d7626c6ed6c960d339a3a0d270941723fa4cfa4fb2
7
- data.tar.gz: b8d8bb09ef31fb5792e482e06a7f75a6c4ea75b5a335762e950fc1088ecdad15327264ade1b12504ad9a96c8391ba4f13959b5517c442ff171896b10b1de1e60
6
+ metadata.gz: 781f2603034ba02007a26678f06738ff5b0691cf2bd45c9d56406c3d4bffe15f375301b52f42f4a5e5bf8fd7fcb2b10b61e61d623cfab5d21c901025cb6f70a6
7
+ data.tar.gz: df7bfefdb22eaf086e25dfc91b233bd3eb02b3c7e658315e05b4d1e92a30e534f626622b7025a8eff244f6b29c1637bb3ed0678a1c04cab0c5ffd180b18df105
data/.gitignore CHANGED
@@ -55,9 +55,6 @@ Gemfile.lock
55
55
  # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
56
  # .rubocop-https?--*
57
57
 
58
- # Project-specific ignores
59
- .rspec
60
-
61
58
  # VSCode
62
59
  .vscode
63
60
 
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## v12.0.0 30th November 2023
2
+
3
+ ### Changed
4
+ - Added multi-database support [#522](https://github.com/gocardless/statesman/pull/522)
5
+ - This now uses the correct ActiveRecord connection for the model or transition in a multi-database environment
6
+
1
7
  ## v11.0.0 3rd November 2023
2
8
 
3
9
  ### Changed
@@ -52,7 +52,7 @@ module Statesman
52
52
  end
53
53
 
54
54
  def database_supports_partial_indexes?
55
- Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
55
+ Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(klass.constantize)
56
56
  end
57
57
 
58
58
  def metadata_default_value
@@ -7,19 +7,15 @@ module Statesman
7
7
  class ActiveRecord
8
8
  JSON_COLUMN_TYPES = %w[json jsonb].freeze
9
9
 
10
- def self.database_supports_partial_indexes?
10
+ def self.database_supports_partial_indexes?(model)
11
11
  # Rails 3 doesn't implement `supports_partial_index?`
12
- if ::ActiveRecord::Base.connection.respond_to?(:supports_partial_index?)
13
- ::ActiveRecord::Base.connection.supports_partial_index?
12
+ if model.connection.respond_to?(:supports_partial_index?)
13
+ model.connection.supports_partial_index?
14
14
  else
15
- ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
15
+ model.connection.adapter_name.casecmp("postgresql").zero?
16
16
  end
17
17
  end
18
18
 
19
- def self.adapter_name
20
- ::ActiveRecord::Base.connection.adapter_name.downcase
21
- end
22
-
23
19
  def initialize(transition_class, parent_model, observer, options = {})
24
20
  serialized = serialized?(transition_class)
25
21
  column_type = transition_class.columns_hash["metadata"].sql_type
@@ -88,10 +84,10 @@ module Statesman
88
84
  default_transition_attributes(to, metadata),
89
85
  )
90
86
 
91
- ::ActiveRecord::Base.transaction(requires_new: true) do
87
+ transition_class.transaction(requires_new: true) do
92
88
  @observer.execute(:before, from, to, transition)
93
89
 
94
- if mysql_gaplock_protection?
90
+ if mysql_gaplock_protection?(transition_class.connection)
95
91
  # We save the transition first with most_recent falsy, then mark most_recent
96
92
  # true after to avoid letting MySQL acquire a next-key lock which can cause
97
93
  # deadlocks.
@@ -130,8 +126,8 @@ module Statesman
130
126
  end
131
127
 
132
128
  def add_after_commit_callback(from, to, transition)
133
- ::ActiveRecord::Base.connection.add_transaction_record(
134
- ActiveRecordAfterCommitWrap.new do
129
+ transition_class.connection.add_transaction_record(
130
+ ActiveRecordAfterCommitWrap.new(transition_class.connection) do
135
131
  @observer.execute(:after_commit, from, to, transition)
136
132
  end,
137
133
  )
@@ -144,7 +140,7 @@ module Statesman
144
140
  # Sets the given transition most_recent = t while unsetting the most_recent of any
145
141
  # previous transitions.
146
142
  def update_most_recents(most_recent_id = nil)
147
- update = build_arel_manager(::Arel::UpdateManager)
143
+ update = build_arel_manager(::Arel::UpdateManager, transition_class)
148
144
  update.table(transition_table)
149
145
  update.where(most_recent_transitions(most_recent_id))
150
146
  update.set(build_most_recents_update_all_values(most_recent_id))
@@ -152,9 +148,11 @@ module Statesman
152
148
  # MySQL will validate index constraints across the intermediate result of an
153
149
  # update. This means we must order our update to deactivate the previous
154
150
  # most_recent before setting the new row to be true.
155
- update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
151
+ if mysql_gaplock_protection?(transition_class.connection)
152
+ update.order(transition_table[:most_recent].desc)
153
+ end
156
154
 
157
- ::ActiveRecord::Base.connection.update(update.to_sql)
155
+ transition_class.connection.update(update.to_sql(transition_class))
158
156
  end
159
157
 
160
158
  def most_recent_transitions(most_recent_id = nil)
@@ -223,7 +221,7 @@ module Statesman
223
221
  if most_recent_id
224
222
  Arel::Nodes::Case.new.
225
223
  when(transition_table[:id].eq(most_recent_id)).then(db_true).
226
- else(not_most_recent_value).to_sql
224
+ else(not_most_recent_value).to_sql(transition_class)
227
225
  else
228
226
  Arel::Nodes::SqlLiteral.new(not_most_recent_value)
229
227
  end
@@ -233,11 +231,11 @@ module Statesman
233
231
  # change in Arel as we move into Rails >6.0.
234
232
  #
235
233
  # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
236
- def build_arel_manager(manager)
234
+ def build_arel_manager(manager, engine)
237
235
  if manager.instance_method(:initialize).arity.zero?
238
236
  manager.new
239
237
  else
240
- manager.new(::ActiveRecord::Base)
238
+ manager.new(engine)
241
239
  end
242
240
  end
243
241
 
@@ -258,7 +256,7 @@ module Statesman
258
256
  end
259
257
 
260
258
  def unique_indexes
261
- ::ActiveRecord::Base.connection.
259
+ transition_class.connection.
262
260
  indexes(transition_class.table_name).
263
261
  select do |index|
264
262
  next unless index.unique
@@ -329,16 +327,16 @@ module Statesman
329
327
  ::ActiveRecord::Base.default_timezone
330
328
  end
331
329
 
332
- def mysql_gaplock_protection?
333
- Statesman.mysql_gaplock_protection?
330
+ def mysql_gaplock_protection?(connection)
331
+ Statesman.mysql_gaplock_protection?(connection)
334
332
  end
335
333
 
336
334
  def db_true
337
- ::ActiveRecord::Base.connection.quote(type_cast(true))
335
+ transition_class.connection.quote(type_cast(true))
338
336
  end
339
337
 
340
338
  def db_false
341
- ::ActiveRecord::Base.connection.quote(type_cast(false))
339
+ transition_class.connection.quote(type_cast(false))
342
340
  end
343
341
 
344
342
  def db_null
@@ -348,7 +346,7 @@ module Statesman
348
346
  # Type casting against a column is deprecated and will be removed in Rails 6.2.
349
347
  # See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
350
348
  def type_cast(value)
351
- ::ActiveRecord::Base.connection.type_cast(value)
349
+ transition_class.connection.type_cast(value)
352
350
  end
353
351
 
354
352
  # Check whether the `most_recent` column allows null values. If it doesn't, set old
@@ -368,9 +366,9 @@ module Statesman
368
366
  end
369
367
 
370
368
  class ActiveRecordAfterCommitWrap
371
- def initialize(&block)
369
+ def initialize(connection, &block)
372
370
  @callback = block
373
- @connection = ::ActiveRecord::Base.connection
371
+ @connection = connection
374
372
  end
375
373
 
376
374
  def self.trigger_transactional_callbacks?
@@ -153,7 +153,7 @@ module Statesman
153
153
  end
154
154
 
155
155
  def db_true
156
- ::ActiveRecord::Base.connection.quote(true)
156
+ model.connection.quote(true)
157
157
  end
158
158
  end
159
159
  end
@@ -15,17 +15,10 @@ module Statesman
15
15
  @adapter_class = adapter_class
16
16
  end
17
17
 
18
- def mysql_gaplock_protection?
19
- return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
20
-
18
+ def mysql_gaplock_protection?(connection)
21
19
  # If our adapter class suggests we're using mysql, enable gaplock protection by
22
20
  # default.
23
- enable_mysql_gaplock_protection if mysql_adapter?(adapter_class)
24
- @mysql_gaplock_protection
25
- end
26
-
27
- def enable_mysql_gaplock_protection
28
- @mysql_gaplock_protection = true
21
+ mysql_adapter?(connection)
29
22
  end
30
23
 
31
24
  private
@@ -34,7 +27,7 @@ module Statesman
34
27
  adapter_name = adapter_name(adapter_class)
35
28
  return false unless adapter_name
36
29
 
37
- adapter_name.start_with?("mysql")
30
+ adapter_name.downcase.start_with?("mysql")
38
31
  end
39
32
 
40
33
  def adapter_name(adapter_class)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "11.0.0"
4
+ VERSION = "12.0.0"
5
5
  end
data/lib/statesman.rb CHANGED
@@ -34,10 +34,8 @@ module Statesman
34
34
  @storage_adapter || Adapters::Memory
35
35
  end
36
36
 
37
- def self.mysql_gaplock_protection?
38
- return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
39
-
40
- @mysql_gaplock_protection = config.mysql_gaplock_protection?
37
+ def self.mysql_gaplock_protection?(connection)
38
+ config.mysql_gaplock_protection?(connection)
41
39
  end
42
40
 
43
41
  def self.config
@@ -21,8 +21,8 @@ namespace :statesman do
21
21
  batch_size = 500
22
22
 
23
23
  parent_class.find_in_batches(batch_size: batch_size) do |models|
24
- ActiveRecord::Base.transaction(requires_new: true) do
25
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
24
+ transition_class.transaction(requires_new: true) do
25
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(transition_class)
26
26
  # Set all transitions' most_recent to FALSE
27
27
  transition_class.where(parent_fk => models.map(&:id)).
28
28
  update_all(most_recent: false, updated_at: updated_at)
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "support/generators_shared_examples"
5
4
  require "generators/statesman/active_record_transition_generator"
6
5
 
7
6
  describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
7
+ before do
8
+ stub_const("Bacon", Class.new(ActiveRecord::Base))
9
+ stub_const("BaconTransition", Class.new(ActiveRecord::Base))
10
+ stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
11
+ stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
12
+ end
13
+
8
14
  it_behaves_like "a generator" do
9
15
  let(:migration_name) { "db/migrate/create_bacon_transitions.rb" }
10
16
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "support/generators_shared_examples"
5
4
  require "generators/statesman/migration_generator"
6
5
 
7
6
  describe Statesman::MigrationGenerator, type: :generator do
7
+ before do
8
+ stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
9
+ stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
10
+ end
11
+
8
12
  it_behaves_like "a generator" do
9
13
  let(:migration_name) { "db/migrate/add_statesman_to_bacon_transitions.rb" }
10
14
  end
data/spec/spec_helper.rb CHANGED
@@ -5,13 +5,14 @@ require "sqlite3"
5
5
  require "mysql2"
6
6
  require "pg"
7
7
  require "active_record"
8
+ require "active_record/database_configurations"
8
9
  # We have to include all of Rails to make rspec-rails work
9
10
  require "rails"
10
11
  require "action_view"
11
12
  require "action_dispatch"
12
13
  require "action_controller"
13
14
  require "rspec/rails"
14
- require "support/active_record"
15
+ require "support/exactly_query_databases"
15
16
  require "rspec/its"
16
17
  require "pry"
17
18
 
@@ -28,10 +29,31 @@ RSpec.configure do |config|
28
29
  if config.exclusion_filter[:active_record]
29
30
  puts "Skipping ActiveRecord tests"
30
31
  else
31
- # Connect to the database for activerecord tests
32
- db_conn_spec = ENV["DATABASE_URL"]
33
- db_conn_spec ||= { adapter: "sqlite3", database: ":memory:" }
34
- ActiveRecord::Base.establish_connection(db_conn_spec)
32
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
33
+
34
+ # We have to parse this to a hash since ActiveRecord::Base.configurations
35
+ # will only consider a single URL config.
36
+ url_config = if ENV["DATABASE_URL"]
37
+ ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.
38
+ new(ENV["DATABASE_URL"]).to_hash.merge({ sslmode: "disable" })
39
+ end
40
+
41
+ db_config = {
42
+ current_env => {
43
+ primary: url_config || {
44
+ adapter: "sqlite3",
45
+ database: "/tmp/statesman.db",
46
+ },
47
+ secondary: url_config || {
48
+ adapter: "sqlite3",
49
+ database: "/tmp/statesman.db",
50
+ },
51
+ },
52
+ }
53
+
54
+ # Connect to the primary database for activerecord tests.
55
+ ActiveRecord::Base.configurations = db_config
56
+ ActiveRecord::Base.establish_connection(:primary)
35
57
 
36
58
  db_adapter = ActiveRecord::Base.connection.adapter_name
37
59
  puts "Running with database adapter '#{db_adapter}'"
@@ -40,6 +62,8 @@ RSpec.configure do |config|
40
62
  ActiveRecord::Migration.verbose = false
41
63
  end
42
64
 
65
+ # Since our primary and secondary connections point to the same database, we don't
66
+ # need to worry about applying these actions to both.
43
67
  config.before(:each, :active_record) do
44
68
  tables = %w[
45
69
  my_active_record_models
@@ -53,6 +77,7 @@ RSpec.configure do |config|
53
77
  ]
54
78
  tables.each do |table_name|
55
79
  sql = "DROP TABLE IF EXISTS #{table_name};"
80
+
56
81
  ActiveRecord::Base.connection.execute(sql)
57
82
  end
58
83
 
@@ -84,3 +109,6 @@ RSpec.configure do |config|
84
109
  end
85
110
  end
86
111
  end
112
+
113
+ # We have to require this after the databases are configured.
114
+ require "support/active_record"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Adapters::ActiveRecordQueries, :active_record do
6
4
  def configure_old(klass, transition_class)
7
5
  klass.define_singleton_method(:transition_class) { transition_class }
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "timecop"
5
4
  require "statesman/adapters/shared_examples"
6
5
  require "statesman/exceptions"
@@ -10,8 +9,6 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
10
9
  prepare_model_table
11
10
  prepare_transitions_table
12
11
 
13
- # MyActiveRecordModelTransition.serialize(:metadata, JSON)
14
-
15
12
  prepare_sti_model_table
16
13
  prepare_sti_transitions_table
17
14
 
@@ -26,8 +23,10 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
26
23
 
27
24
  after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
28
25
 
26
+ let(:model_class) { MyActiveRecordModel }
27
+ let(:transition_class) { MyActiveRecordModelTransition }
29
28
  let(:observer) { double(Statesman::Machine, execute: nil) }
30
- let(:model) { MyActiveRecordModel.create(current_state: :pending) }
29
+ let(:model) { model_class.create(current_state: :pending) }
31
30
 
32
31
  it_behaves_like "an adapter", described_class, MyActiveRecordModelTransition
33
32
 
@@ -36,8 +35,8 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
36
35
  before do
37
36
  metadata_column = double
38
37
  allow(metadata_column).to receive_messages(sql_type: "")
39
- allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
40
- { "metadata" => metadata_column })
38
+ allow(MyActiveRecordModelTransition).
39
+ to receive_messages(columns_hash: { "metadata" => metadata_column })
41
40
  expect(MyActiveRecordModelTransition).
42
41
  to receive(:type_for_attribute).with("metadata").
43
42
  and_return(ActiveRecord::Type::Value.new)
@@ -105,13 +104,15 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
105
104
  describe "#create" do
106
105
  subject(:transition) { create }
107
106
 
108
- let!(:adapter) do
109
- described_class.new(MyActiveRecordModelTransition, model, observer)
110
- end
107
+ let!(:adapter) { described_class.new(transition_class, model, observer) }
111
108
  let(:from) { :x }
112
109
  let(:to) { :y }
113
110
  let(:create) { adapter.create(from, to) }
114
111
 
112
+ it "only connects to the primary database" do
113
+ expect { create }.to exactly_query_databases({ primary: [:writing] })
114
+ end
115
+
115
116
  context "when there is a race" do
116
117
  it "raises a TransitionConflictError" do
117
118
  adapter2 = adapter.dup
@@ -119,7 +120,8 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
119
120
  adapter.last
120
121
  adapter2.create(:y, :z)
121
122
  expect { adapter.create(:y, :z) }.
122
- to raise_exception(Statesman::TransitionConflictError)
123
+ to raise_exception(Statesman::TransitionConflictError).
124
+ and exactly_query_databases({ primary: [:writing] })
123
125
  end
124
126
 
125
127
  it "does not pollute the state when the transition fails" do
@@ -343,12 +345,34 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
343
345
  end
344
346
  end
345
347
  end
348
+
349
+ context "when using the secondary database" do
350
+ let(:model_class) { SecondaryActiveRecordModel }
351
+ let(:transition_class) { SecondaryActiveRecordModelTransition }
352
+
353
+ it "doesn't connect to the primary database" do
354
+ expect { create }.to exactly_query_databases({ secondary: [:writing] })
355
+ expect(adapter.last.to_state).to eq("y")
356
+ end
357
+
358
+ context "when there is a race" do
359
+ it "raises a TransitionConflictError and uses the correct database" do
360
+ adapter2 = adapter.dup
361
+ adapter2.create(:x, :y)
362
+ adapter.last
363
+ adapter2.create(:y, :z)
364
+
365
+ expect { adapter.create(:y, :z) }.
366
+ to raise_exception(Statesman::TransitionConflictError).
367
+ and exactly_query_databases({ secondary: [:writing] })
368
+ end
369
+ end
370
+ end
346
371
  end
347
372
 
348
373
  describe "#last" do
349
- let(:adapter) do
350
- described_class.new(MyActiveRecordModelTransition, model, observer)
351
- end
374
+ let(:transition_class) { MyActiveRecordModelTransition }
375
+ let(:adapter) { described_class.new(transition_class, model, observer) }
352
376
 
353
377
  context "with a previously looked up transition" do
354
378
  before { adapter.create(:x, :y) }
@@ -365,8 +389,19 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
365
389
  before { adapter.create(:y, :z, []) }
366
390
 
367
391
  it "retrieves the new transition from the database" do
392
+ expect { adapter.last.to_state }.to exactly_query_databases({ primary: [:writing] })
368
393
  expect(adapter.last.to_state).to eq("z")
369
394
  end
395
+
396
+ context "when using the secondary database" do
397
+ let(:model_class) { SecondaryActiveRecordModel }
398
+ let(:transition_class) { SecondaryActiveRecordModelTransition }
399
+
400
+ it "retrieves the new transition from the database" do
401
+ expect { adapter.last.to_state }.to exactly_query_databases({ secondary: [:writing] })
402
+ expect(adapter.last.to_state).to eq("z")
403
+ end
404
+ end
370
405
  end
371
406
 
372
407
  context "when a new transition has been created elsewhere" do
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "json"
5
4
 
6
5
  describe Statesman::Adapters::ActiveRecordTransition do
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "statesman/adapters/shared_examples"
5
4
  require "statesman/adapters/memory_transition"
6
5
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "statesman/adapters/memory_transition"
5
4
 
6
5
  describe Statesman::Adapters::MemoryTransition do
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  # All adpators must define seven methods:
6
4
  # initialize: Accepts a transition class, parent model and state_attr.
7
5
  # transition_class: Returns the transition class object passed to initialize.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Adapters::TypeSafeActiveRecordQueries, :active_record do
6
4
  def configure(klass, transition_class)
7
5
  klass.send(:extend, described_class)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Callback do
6
4
  let(:cb_lambda) { -> {} }
7
5
  let(:callback) do
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Config do
6
4
  let(:instance) { described_class.new }
7
5
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe "Exceptions" do
6
4
  describe "InvalidStateError" do
7
5
  subject(:error) { Statesman::InvalidStateError.new }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Guard do
6
4
  let(:callback) { -> {} }
7
5
  let(:guard) { described_class.new(from: nil, to: nil, callback: callback) }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Machine do
6
4
  let(:machine) { Class.new { include Statesman::Machine } }
7
5
  let(:my_model) { Class.new { attr_accessor :current_state }.new }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Utils do
6
4
  describe ".rails_major_version" do
7
5
  subject { described_class.rails_major_version }
@@ -81,7 +81,7 @@ class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
81
81
  t.text :metadata, default: "{}"
82
82
  end
83
83
 
84
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
84
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
85
85
  t.boolean :most_recent, default: true, null: false
86
86
  else
87
87
  t.boolean :most_recent, default: true
@@ -98,7 +98,7 @@ class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
98
98
  %i[my_active_record_model_id sort_key],
99
99
  unique: true, name: "sort_key_index"
100
100
 
101
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
101
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
102
102
  add_index :my_active_record_model_transitions,
103
103
  %i[my_active_record_model_id most_recent],
104
104
  unique: true,
@@ -134,6 +134,48 @@ class OtherActiveRecordModelTransition < ActiveRecord::Base
134
134
  belongs_to :other_active_record_model
135
135
  end
136
136
 
137
+ class SecondaryRecord < ActiveRecord::Base
138
+ self.abstract_class = true
139
+
140
+ connects_to database: { writing: :secondary, reading: :secondary }
141
+ end
142
+
143
+ class SecondaryActiveRecordModelTransition < SecondaryRecord
144
+ self.table_name = "my_active_record_model_transitions"
145
+
146
+ include Statesman::Adapters::ActiveRecordTransition
147
+
148
+ belongs_to :my_active_record_model,
149
+ class_name: "SecondaryActiveRecordModel",
150
+ foreign_key: "my_active_record_model_transition_id"
151
+ end
152
+
153
+ class SecondaryActiveRecordModel < SecondaryRecord
154
+ self.table_name = "my_active_record_models"
155
+
156
+ has_many :my_active_record_model_transitions,
157
+ class_name: "SecondaryActiveRecordModelTransition",
158
+ foreign_key: "my_active_record_model_id",
159
+ autosave: false
160
+
161
+ alias_method :transitions, :my_active_record_model_transitions
162
+
163
+ include Statesman::Adapters::ActiveRecordQueries[
164
+ transition_class: SecondaryActiveRecordModelTransition,
165
+ initial_state: :initial
166
+ ]
167
+
168
+ def state_machine
169
+ @state_machine ||= MyStateMachine.new(
170
+ self, transition_class: SecondaryActiveRecordModelTransition
171
+ )
172
+ end
173
+
174
+ def metadata
175
+ super || {}
176
+ end
177
+ end
178
+
137
179
  class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS
138
180
  def change
139
181
  create_table :other_active_record_models do |t|
@@ -158,7 +200,7 @@ class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
158
200
  t.text :metadata, default: "{}"
159
201
  end
160
202
 
161
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
203
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
162
204
  t.boolean :most_recent, default: true, null: false
163
205
  else
164
206
  t.boolean :most_recent, default: true
@@ -171,7 +213,7 @@ class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
171
213
  %i[other_active_record_model_id sort_key],
172
214
  unique: true, name: "other_sort_key_index"
173
215
 
174
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
216
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
175
217
  add_index :other_active_record_model_transitions,
176
218
  %i[other_active_record_model_id most_recent],
177
219
  unique: true,
@@ -253,7 +295,7 @@ class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
253
295
  t.text :metadata, default: "{}"
254
296
  end
255
297
 
256
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
298
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
257
299
  t.boolean :most_recent, default: true, null: false
258
300
  else
259
301
  t.boolean :most_recent, default: true
@@ -265,7 +307,7 @@ class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
265
307
  add_index :my_namespace_my_active_record_model_transitions, :sort_key,
266
308
  unique: true, name: "my_namespaced_key"
267
309
 
268
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
310
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
269
311
  add_index :my_namespace_my_active_record_model_transitions,
270
312
  %i[my_active_record_model_id most_recent],
271
313
  unique: true,
@@ -342,7 +384,7 @@ class CreateStiActiveRecordModelTransitionMigration < MIGRATION_CLASS
342
384
  t.text :metadata, default: "{}"
343
385
  end
344
386
 
345
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
387
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
346
388
  t.boolean :most_recent, default: true, null: false
347
389
  else
348
390
  t.boolean :most_recent, default: true
@@ -355,7 +397,7 @@ class CreateStiActiveRecordModelTransitionMigration < MIGRATION_CLASS
355
397
  %i[type sti_active_record_model_id sort_key],
356
398
  unique: true, name: "sti_sort_key_index"
357
399
 
358
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
400
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
359
401
  add_index :sti_active_record_model_transitions,
360
402
  %i[type sti_active_record_model_id most_recent],
361
403
  unique: true,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `expected_dbs` should be a Hash of the form:
4
+ # {
5
+ # primary: [:writing, :reading],
6
+ # replica: [:reading],
7
+ # }
8
+ RSpec::Matchers.define :exactly_query_databases do |expected_dbs|
9
+ match do |block|
10
+ @expected_dbs = expected_dbs.transform_values(&:to_set).with_indifferent_access
11
+ @actual_dbs = Hash.new { |h, k| h[k] = Set.new }.with_indifferent_access
12
+
13
+ ActiveSupport::Notifications.
14
+ subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
15
+ pool = payload.fetch(:connection).pool
16
+
17
+ next if pool.is_a?(ActiveRecord::ConnectionAdapters::NullPool)
18
+
19
+ name = pool.db_config.name
20
+ role = pool.role
21
+
22
+ @actual_dbs[name] << role
23
+ end
24
+
25
+ block.call
26
+
27
+ @actual_dbs == @expected_dbs
28
+ end
29
+
30
+ failure_message do |_block|
31
+ "expected to query exactly #{@expected_dbs}, but queried #{@actual_dbs}"
32
+ end
33
+
34
+ supports_block_expectations
35
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statesman
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.0.0
4
+ version: 12.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-03 00:00:00.000000000 Z
11
+ date: 2023-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter
@@ -214,6 +214,7 @@ files:
214
214
  - ".github/dependabot.yml"
215
215
  - ".github/workflows/tests.yml"
216
216
  - ".gitignore"
217
+ - ".rspec"
217
218
  - ".rubocop.yml"
218
219
  - ".rubocop_todo.yml"
219
220
  - ".ruby-version"
@@ -267,6 +268,7 @@ files:
267
268
  - spec/statesman/machine_spec.rb
268
269
  - spec/statesman/utils_spec.rb
269
270
  - spec/support/active_record.rb
271
+ - spec/support/exactly_query_databases.rb
270
272
  - spec/support/generators_shared_examples.rb
271
273
  - statesman.gemspec
272
274
  homepage: https://github.com/gocardless/statesman