statesman 11.0.0 → 12.0.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 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