Manual connection switching API on `ActiveRecord::Relation`

At Buildkite, we’ve got Active Record set up with multiple databases, including some horizontal sharding, and we utilise automatic and (granular) manual role switching quite heavily.

A pattern that arises with such a setup is manually switching the connection for specific write queries when in a request that has been automatically switched to use the reader role (e.g., GET/HEAD). Conversely, we also manually switch the connection for specific, large read queries to reduce load on the writer when the request isn’t readonly. Example:

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { reading: :primary_replica, writing: :primary }
  def self.with_replica(&block) = connected_to(role: :reading, &block)
end

# app/models/sub_abstract_record.rb

class SubAbstractRecord < ApplicationRecord
  self.abstract_class = true
  connects_to shards: {
    shard_one: { writing: :shard_one, reading: :shard_one_replica },
    shard_two: { writing: :shard_two, reading: :shard_two_replica }
  }
end

# app/models/application_model.rb

class ApplicationModel < ApplicationRecord; end

# app/models/sub_model.rb

class SubModel < SubAbstractRecord; end
# In a POST request

...

record = SubAbstractRecord.with_replica do
  SubModel.where(...).find_by(...)
end

...

records = ApplicationRecord.with_replica do
  ApplicationModel.where(...).to_a
end

...

My feature request is for a relation API that allows switching the connection within the relation/query itself (at the point of execution), avoiding the need to wrap them in blocks. Example:

# In a POST request

...

record = SubModel.connected_to(role: :reading).where(...).find_by(...)

...

records = ApplicationModel.connected_to(role: :reading).where(...).to_a

...

We could then abstract this into a class method and it’ll be accessible as a scope:

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { reading: :primary_replica, writing: :primary }
  # for relations
  def self.with_replica = connected_to(role: :reading)
  # for blocks
  def self.with_replica(&block) = connected_to(role: :reading, &block)
end
# In a POST request

...

record = ApplicationModel.with_replica.where(...).find_by(...)

...

records = ApplicationModel.with_replica.where(...).to_a

...

A side effect of such an implementation would be that we would enable role switching on non-base/abstract classes which is currently not allowed, and infer the target abstract class, which doesn’t seem difficult to me.

I hacked on this a bit and managed to get it working a while back, so opening this to see if there’s any interest in such a feature and to get any feedback from the core team so I can send a PR. TIA!