class Tapioca::Dsl::Compilers::ActiveRecordRelations

~~~
end
end
Elem = type_member { { fixed: ::Post } }
def to_ary; end
sig { returns(T::Array) }
def to_a; end
sig { returns(T::Array) }
include GeneratedRelationMethods
include CommonRelationMethods
class PrivateRelation < ::ActiveRecord::Relation
end
# …
def <<(*records); end
end
.returns(PrivateCollectionProxy)
params(records: T.any(::Post, T::Array, T::Array))
sig do
include GeneratedAssociationRelationMethods
include CommonRelationMethods
class PrivateCollectionProxy < ::ActiveRecord::Associations::CollectionProxy
end
Elem = type_member { { fixed: ::Post } }
def to_ary; end
sig { returns(T::Array) }
def to_a; end
sig { returns(T::Array) }
include GeneratedAssociationRelationMethods
include CommonRelationMethods
class PrivateAssociationRelation < ::ActiveRecord::AssociationRelation
end
def where(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
# …
def all; end
sig { returns(PrivateRelation) }
module GeneratedRelationMethods
end
def where(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
# …
def all; end
sig { returns(PrivateAssociationRelation) }
module GeneratedAssociationRelationMethods
end
# …
def any?(&block); end
sig { params(block: T.nilable(T.proc.params(record: ::Post).returns(T.untyped))).returns(T::Boolean) }
module CommonRelationMethods
extend GeneratedRelationMethods
extend CommonRelationMethods
class Post
# typed: true
# post.rbi
~~~rbi
this compiler will produce the RBI file ‘post.rbi` with the following content:
~~~
end
class Post < ApplicationRecord
~~~rb
For example, with the following `ActiveRecord::Base` subclass:
make the runtime checks fail.
For that reason, these types cannot be used in user code or in `sig`s inside Ruby files, since that will
exist at runtime, and their counterparts that do exist at runtime are marked `private_constant` anyway.
that they represent private subconstants of the Active Record model. As such, these types do not
CAUTION: The generated relation classes are named `PrivateXXX` intentionally to reflect the fact
`Model` class.
`Model::PrivateRelation` modules, so that, for example, `find_by` and `all` can be chained off of the
Additionally, the actual `Model` class extends both `Model::CommonRelationMethods` and
This module is used to reduce the replication of methods between the previous two modules.
instance), regardless of what kind of relation it is called on, and so belongs in this module.
relation in their return type. For example, `find_by!` will always return the same type (a `Model`
3. `Model::CommonRelationMethods` holds all the relation methods that do not depend on the type of
with that return type in this module.
always return a `Model::PrivateAssociationRelation` instance, thus the signature of `all` is defined
`Model::PrivateAssociationRelation` or an instance of `Model::PrivateCollectionProxy` class will
of `Model::PrivateAssociationRelation`. For example, calling `all` on an instance of
2. `Model::GeneratedAssociationRelationMethods` holds all the relation methods with the return type
signature of `all` is defined with that return type in this module.
`Model::PrivateRelation` class will always return a `Model::PrivateRelation` instance, thus the
`Model::PrivateRelation`. For example, calling `all` on the `Model` class or an instance of
1. `Model::GeneratedRelationMethods` holds all the relation methods with the return type of
and the following modules:
etc new `Model` instances in the collection.
This class represents a collection of `Model` instances with some extra methods to `build`, `create`,
whose methods which return a relation will always return a `Model::PrivateAssociationRelation` instance.
This synthetic class represents a relation on a plural association of type `Model` (e.g. `foo.models`)
3. `Model::PrivateCollectionProxy` that subclasses from `ActiveRecord::Associations::CollectionProxy`.
for this relation.
class and the previous one is mainly that an association relation also keeps track of the resource association
return a relation will always return a `Model::PrivateAssociationRelation` instance. The difference between this
class represents a relation on a singular association of type `Model` (e.g. `foo.model`) whose methods which
2. `Model::PrivateAssociationRelation` that subclasses `ActiveRecord::AssociationRelation`. This synthetic
a relation on `Model` whose methods which return a relation always return a `Model::PrivateRelation` instance.
1. A `Model::PrivateRelation` that subclasses `ActiveRecord::Relation`. This synthetic class represents
For a given model `Model`, we generate the following classes:
The compiler defines 3 (synthetic) modules and 3 (synthetic) classes to represent relations properly.<br><br>(api.rubyonrails.org/classes/ActiveRecord/Calculations.html) methods.<br>(api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html), and<br>(api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html),<br>[query](api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html),
[collection proxy](https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html),<br>(api.rubyonrails.org/classes/ActiveRecord/Relation.html),
`ActiveRecord::Base` and adds
`Tapioca::Dsl::Compilers::ActiveRecordRelations` decorates RBI files for subclasses of

def association_relation_methods_module

def association_relation_methods_module
  @association_relation_methods_module ||= T.let(
    model.create_module(AssociationRelationMethodsModuleName),
    T.nilable(RBI::Scope),
  )
end

def bang_method?(method_name)

def bang_method?(method_name)
  method_name.to_s.end_with?("!")
end

def common_relation_methods_module

def common_relation_methods_module
  @common_relation_methods_module ||= T.let(
    model.create_module(CommonRelationMethodsModuleName),
    T.nilable(RBI::Scope),
  )
end

def constant_name

def constant_name
  @constant_name ||= T.let(T.must(qualified_name_of(constant)), T.nilable(String))
end

def create_association_relation_class

def create_association_relation_class
  superclass = "::ActiveRecord::AssociationRelation"
  # Association subclasses include the generated association relation module
  model.create_class(AssociationRelationClassName, superclass_name: superclass) do |klass|
    klass.create_include(CommonRelationMethodsModuleName)
    klass.create_include(AssociationRelationMethodsModuleName)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
    TO_ARRAY_METHODS.each do |method_name|
      klass.create_method(method_name.to_s, return_type: "T::Array[#{constant_name}]")
    end
  end
  create_association_relation_group_chain_class
  create_association_relation_where_chain_class
end

def create_association_relation_group_chain_class

def create_association_relation_group_chain_class
  model.create_class(
    AssociationRelationGroupChainClassName,
    superclass_name: AssociationRelationClassName,
  ) do |klass|
    create_group_chain_methods(klass)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
  end
end

def create_association_relation_methods

def create_association_relation_methods
  returning_type = "T.nilable(T.any(T::Array[Symbol], FalseClass))"
  unique_by_type = "T.nilable(T.any(T::Array[Symbol], Symbol))"
  ASSOCIATION_METHODS.each do |method_name|
    case method_name
    when :insert_all, :insert_all!, :upsert_all
      parameters = [
        create_param("attributes", type: "T::Array[Hash]"),
        create_kw_opt_param("returning", type: returning_type, default: "nil"),
      ]
      # Bang methods don't have the `unique_by` parameter
      unless bang_method?(method_name)
        parameters << create_kw_opt_param("unique_by", type: unique_by_type, default: "nil")
      end
      association_relation_methods_module.create_method(
        method_name.to_s,
        parameters: parameters,
        return_type: "ActiveRecord::Result",
      )
    when :insert, :insert!, :upsert
      parameters = [
        create_param("attributes", type: "Hash"),
        create_kw_opt_param("returning", type: returning_type, default: "nil"),
      ]
      # Bang methods don't have the `unique_by` parameter
      unless bang_method?(method_name)
        parameters << create_kw_opt_param("unique_by", type: unique_by_type, default: "nil")
      end
      association_relation_methods_module.create_method(
        method_name.to_s,
        parameters: parameters,
        return_type: "ActiveRecord::Result",
      )
    when :proxy_association
      # skip - private method
    end
  end
end

def create_association_relation_where_chain_class

def create_association_relation_where_chain_class
  model.create_class(AssociationRelationWhereChainClassName) do |klass|
    create_where_chain_methods(klass, AssociationRelationClassName)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
  end
end

def create_classes_and_includes

def create_classes_and_includes
  model.create_extend(CommonRelationMethodsModuleName)
  # The model always extends the generated relation module
  model.create_extend(RelationMethodsModuleName)
  # Type the `to_ary` method as returning `NilClass` so that flatten stops recursing
  # See https://github.com/sorbet/sorbet/pull/4706 for details
  model.create_method("to_ary", return_type: "NilClass", visibility: RBI::Private.new)
  create_relation_class
  create_association_relation_class
  create_collection_proxy_class
end

def create_collection_proxy_class

def create_collection_proxy_class
  superclass = "::ActiveRecord::Associations::CollectionProxy"
  # The relation subclass includes the generated association relation module
  model.create_class(AssociationsCollectionProxyClassName, superclass_name: superclass) do |klass|
    klass.create_include(CommonRelationMethodsModuleName)
    klass.create_include(AssociationRelationMethodsModuleName)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
    TO_ARRAY_METHODS.each do |method_name|
      klass.create_method(method_name.to_s, return_type: "T::Array[#{constant_name}]")
    end
    create_collection_proxy_methods(klass)
  end
end

def create_collection_proxy_methods(klass)

def create_collection_proxy_methods(klass)
  # For these cases, it is valid to pass:
  # - a model instance, thus `Model`
  # - a model collection which can be:
  #   - an array of models, thus `T::Enumerable[Model]`
  #   - an association relation of a model, thus `T::Enumerable[Model]`
  #   - a collection proxy of a model, thus, again, a `T::Enumerable[Model]`
  #   - a collection of relations or collection proxies, thus `T::Enumerable[T::Enumerable[Model]]`
  #   - or, any mix of the above, thus `T::Enumerable[T.any(Model, T::Enumerable[Model])]`
  # which altogether gives us:
  #   `T.any(Model, T::Enumerable[T.any(Model, T::Enumerable[Model])])`
  model_collection =
    "T.any(#{constant_name}, T::Enumerable[T.any(#{constant_name}, T::Enumerable[#{constant_name}])])"
  # For these cases, it is valid to pass the above kind of things, but also:
  # - a model identifier, which can be:
  #   - a numeric id, thus `Integer`
  #   - a string id, thus `String`
  # - a collection of identifiers
  #   - a collection of identifiers, thus `T::Enumerable[T.any(Integer, String)]`
  # which, coupled with the above case, gives us:
  #   `T.any(Model, Integer, String, T::Enumerable[T.any(Model, Integer, String, T::Enumerable[Model])])`
  model_or_id_collection =
    "T.any(#{constant_name}, Integer, String" \
      ", T::Enumerable[T.any(#{constant_name}, Integer, String, T::Enumerable[#{constant_name}])])"
  COLLECTION_PROXY_METHODS.each do |method_name|
    case method_name
    when :<<, :append, :concat, :prepend, :push
      klass.create_method(
        method_name.to_s,
        parameters: [
          create_rest_param("records", type: model_collection),
        ],
        return_type: AssociationsCollectionProxyClassName,
      )
    when :clear
      klass.create_method(
        method_name.to_s,
        return_type: AssociationsCollectionProxyClassName,
      )
    when :delete, :destroy
      klass.create_method(
        method_name.to_s,
        parameters: [
          create_rest_param("records", type: model_or_id_collection),
        ],
        return_type: "T::Array[#{constant_name}]",
      )
    when :load_target
      klass.create_method(
        method_name.to_s,
        return_type: "T::Array[#{constant_name}]",
      )
    when :replace
      klass.create_method(
        method_name.to_s,
        parameters: [
          create_param("other_array", type: model_collection),
        ],
        return_type: "T::Array[#{constant_name}]",
      )
    when :reset_scope
      # skip
    when :scope
      klass.create_method(
        method_name.to_s,
        return_type: AssociationRelationClassName,
      )
    when :target
      klass.create_method(
        method_name.to_s,
        return_type: "T::Array[#{constant_name}]",
      )
    end
  end
end

def create_common_method(name, parameters: [], return_type: nil)

def create_common_method(name, parameters: [], return_type: nil)
  common_relation_methods_module.create_method(
    name.to_s,
    parameters: parameters,
    return_type: return_type || "void",
  )
end

def create_common_methods

def create_common_methods
  create_common_method(
    "destroy_all",
    return_type: "T::Array[#{constant_name}]",
  )
  FINDER_METHODS.each do |method_name|
    case method_name
    when :exists?
      create_common_method(
        "exists?",
        parameters: [
          create_opt_param("conditions", type: "T.untyped", default: ":none"),
        ],
        return_type: "T::Boolean",
      )
    when :include?, :member?
      create_common_method(
        method_name,
        parameters: [
          create_param("record", type: "T.untyped"),
        ],
        return_type: "T::Boolean",
      )
    when :find
      id_types = ID_TYPES
      if constant.table_exists?
        primary_key_type = constant.type_for_attribute(constant.primary_key)
        type = Tapioca::Dsl::Helpers::ActiveModelTypeHelper.type_for(primary_key_type)
        type = RBIHelper.as_non_nilable_type(type)
        id_types = ID_TYPES.union([type]) if type != "T.untyped"
      end
      id_types = "T.any(#{id_types.to_a.join(", ")})"
      if constant.try(:composite_primary_key?)
        id_types = "T::Array[#{id_types}]"
      end
      array_type = "T::Array[#{id_types}]"
      common_relation_methods_module.create_method("find") do |method|
        method.add_opt_param("args", "nil")
        method.add_block_param("block")
        method.add_sig do |sig|
          sig.add_param("args", id_types)
          sig.return_type = constant_name
        end
        method.add_sig do |sig|
          sig.add_param("args", array_type)
          sig.return_type = "T::Enumerable[#{constant_name}]"
        end
        method.add_sig do |sig|
          sig.add_param("args", "NilClass")
          sig.add_param("block", "T.proc.params(object: #{constant_name}).void")
          sig.return_type = as_nilable_type(constant_name)
        end
      end
    when :find_by
      create_common_method(
        "find_by",
        parameters: [
          create_rest_param("args", type: "T.untyped"),
        ],
        return_type: as_nilable_type(constant_name),
      )
    when :find_by!
      create_common_method(
        "find_by!",
        parameters: [
          create_rest_param("args", type: "T.untyped"),
        ],
        return_type: constant_name,
      )
    when :find_sole_by
      create_common_method(
        "find_sole_by",
        parameters: [
          create_param("arg", type: "T.untyped"),
          create_rest_param("args", type: "T.untyped"),
        ],
        return_type: constant_name,
      )
    when :sole
      create_common_method(
        "sole",
        parameters: [],
        return_type: constant_name,
      )
    when :first, :last, :take
      common_relation_methods_module.create_method(method_name.to_s) do |method|
        method.add_opt_param("limit", "nil")
        method.add_sig do |sig|
          sig.return_type = as_nilable_type(constant_name)
        end
        method.add_sig do |sig|
          sig.add_param("limit", "Integer")
          sig.return_type = "T::Array[#{constant_name}]"
        end
      end
    when :raise_record_not_found_exception!
      # skip
    else
      return_type = if bang_method?(method_name)
        constant_name
      else
        as_nilable_type(constant_name)
      end
      create_common_method(
        method_name,
        return_type: return_type,
      )
    end
  end
  SIGNED_FINDER_METHODS.each do |method_name|
    case method_name
    when :find_signed
      create_common_method(
        "find_signed",
        parameters: [
          create_param("signed_id", type: "T.untyped"),
          create_kw_opt_param("purpose", type: "T.untyped", default: "nil"),
        ],
        return_type: as_nilable_type(constant_name),
      )
    when :find_signed!
      create_common_method(
        "find_signed!",
        parameters: [
          create_param("signed_id", type: "T.untyped"),
          create_kw_opt_param("purpose", type: "T.untyped", default: "nil"),
        ],
        return_type: constant_name,
      )
    end
  end
  CALCULATION_METHODS.each do |method_name|
    case method_name
    when :average, :maximum, :minimum
      create_common_method(
        method_name,
        parameters: [
          create_param("column_name", type: "T.any(String, Symbol)"),
        ],
        return_type: method_name == :average ? "T.any(Integer, Float, BigDecimal)" : "T.untyped",
      )
    when :calculate
      create_common_method(
        "calculate",
        parameters: [
          create_param("operation", type: "Symbol"),
          create_param("column_name", type: "T.any(String, Symbol)"),
        ],
        return_type: "T.any(Integer, Float, BigDecimal)",
      )
    when :count
      common_relation_methods_module.create_method(method_name.to_s) do |method|
        method.add_opt_param("column_name", "nil")
        method.add_block_param("block")
        method.add_sig do |sig|
          sig.add_param("column_name", "T.nilable(T.any(String, Symbol))")
          sig.return_type = "Integer"
        end
        method.add_sig do |sig|
          sig.add_param("column_name", "NilClass")
          sig.add_param("block", "T.proc.params(object: #{constant_name}).void")
          sig.return_type = "Integer"
        end
      end
    when :ids
      create_common_method("ids", return_type: "Array")
    when :pick, :pluck
      create_common_method(
        method_name,
        parameters: [
          create_rest_param("column_names", type: "T.untyped"),
        ],
        return_type: "T.untyped",
      )
    when :sum
      common_relation_methods_module.create_method(method_name.to_s) do |method|
        method.add_opt_param("initial_value_or_column", "nil")
        method.add_block_param("block")
        method.add_sig do |sig|
          sig.add_param("initial_value_or_column", "T.untyped")
          sig.return_type = "T.any(Integer, Float, BigDecimal)"
        end
        method.add_sig(type_params: ["U"]) do |sig|
          sig.add_param("initial_value_or_column", "T.nilable(T.type_parameter(:U))")
          sig.add_param("block", "T.proc.params(object: #{constant_name}).returns(T.type_parameter(:U))")
          sig.return_type = "T.type_parameter(:U)"
        end
      end
    end
  end
  BATCHES_METHODS.each do |method_name|
    case method_name
    when :find_each
      order = ActiveRecord::Batches.instance_method(:find_each).parameters.include?([:key, :order])
      common_relation_methods_module.create_method("find_each") do |method|
        method.add_kw_opt_param("start", "nil")
        method.add_kw_opt_param("finish", "nil")
        method.add_kw_opt_param("batch_size", "1000")
        method.add_kw_opt_param("error_on_ignore", "nil")
        method.add_kw_opt_param("order", ":asc") if order
        method.add_block_param("block")
        method.add_sig do |sig|
          sig.add_param("start", "T.untyped")
          sig.add_param("finish", "T.untyped")
          sig.add_param("batch_size", "Integer")
          sig.add_param("error_on_ignore", "T.untyped")
          sig.add_param("order", "Symbol") if order
          sig.add_param("block", "T.proc.params(object: #{constant_name}).void")
          sig.return_type = "void"
        end
        method.add_sig do |sig|
          sig.add_param("start", "T.untyped")
          sig.add_param("finish", "T.untyped")
          sig.add_param("batch_size", "Integer")
          sig.add_param("error_on_ignore", "T.untyped")
          sig.add_param("order", "Symbol") if order
          sig.return_type = "T::Enumerator[#{constant_name}]"
        end
      end
    when :find_in_batches
      order = ActiveRecord::Batches.instance_method(:find_in_batches).parameters.include?([:key, :order])
      common_relation_methods_module.create_method("find_in_batches") do |method|
        method.add_kw_opt_param("start", "nil")
        method.add_kw_opt_param("finish", "nil")
        method.add_kw_opt_param("batch_size", "1000")
        method.add_kw_opt_param("error_on_ignore", "nil")
        method.add_kw_opt_param("order", ":asc") if order
        method.add_block_param("block")
        method.add_sig do |sig|
          sig.add_param("start", "T.untyped")
          sig.add_param("finish", "T.untyped")
          sig.add_param("batch_size", "Integer")
          sig.add_param("error_on_ignore", "T.untyped")
          sig.add_param("order", "Symbol") if order
          sig.add_param("block", "T.proc.params(object: T::Array[#{constant_name}]).void")
          sig.return_type = "void"
        end
        method.add_sig do |sig|
          sig.add_param("start", "T.untyped")
          sig.add_param("finish", "T.untyped")
          sig.add_param("batch_size", "Integer")
          sig.add_param("error_on_ignore", "T.untyped")
          sig.add_param("order", "Symbol") if order
          sig.return_type = "T::Enumerator[T::Enumerator[#{constant_name}]]"
        end
      end
    when :in_batches
      order = ActiveRecord::Batches.instance_method(:in_batches).parameters.include?([:key, :order])
      use_ranges = ActiveRecord::Batches.instance_method(:in_batches).parameters.include?([:key, :use_ranges])
      common_relation_methods_module.create_method("in_batches") do |method|
        method.add_kw_opt_param("of", "1000")
        method.add_kw_opt_param("start", "nil")
        method.add_kw_opt_param("finish", "nil")
        method.add_kw_opt_param("load", "false")
        method.add_kw_opt_param("error_on_ignore", "nil")
        method.add_kw_opt_param("order", ":asc") if order
        method.add_kw_opt_param("use_ranges", "nil") if use_ranges
        method.add_block_param("block")
        method.add_sig do |sig|
          sig.add_param("of", "Integer")
          sig.add_param("start", "T.untyped")
          sig.add_param("finish", "T.untyped")
          sig.add_param("load", "T.untyped")
          sig.add_param("error_on_ignore", "T.untyped")
          sig.add_param("order", "Symbol") if order
          sig.add_param("use_ranges", "T.untyped") if use_ranges
          sig.add_param("block", "T.proc.params(object: #{RelationClassName}).void")
          sig.return_type = "void"
        end
        method.add_sig do |sig|
          sig.add_param("of", "Integer")
          sig.add_param("start", "T.untyped")
          sig.add_param("finish", "T.untyped")
          sig.add_param("load", "T.untyped")
          sig.add_param("error_on_ignore", "T.untyped")
          sig.add_param("order", "Symbol") if order
          sig.add_param("use_ranges", "T.untyped") if use_ranges
          sig.return_type = "::ActiveRecord::Batches::BatchEnumerator"
        end
      end
    end
  end
  ENUMERABLE_QUERY_METHODS.each do |method_name|
    block_type = "T.nilable(T.proc.params(record: #{constant_name}).returns(T.untyped))"
    create_common_method(
      method_name,
      parameters: [
        create_block_param("block", type: block_type),
      ],
      return_type: "T::Boolean",
    )
  end
  FIND_OR_CREATE_METHODS.each do |method_name|
    common_relation_methods_module.create_method(method_name.to_s) do |method|
      method.add_param("attributes")
      method.add_block_param("block")
      # `T.untyped` matches `T::Array[T.untyped]` so the array signature
      # must be defined first for Sorbet to pick it, if valid.
      method.add_sig do |sig|
        sig.add_param("attributes", "T::Array[T.untyped]")
        sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)")
        sig.return_type = "T::Array[#{constant_name}]"
      end
      method.add_sig do |sig|
        sig.add_param("attributes", "T.untyped")
        sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)")
        sig.return_type = constant_name
      end
    end
  end
  BUILDER_METHODS.each do |method_name|
    common_relation_methods_module.create_method(method_name.to_s) do |method|
      method.add_opt_param("attributes", "nil")
      method.add_block_param("block")
      method.add_sig do |sig|
        sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)")
        sig.return_type = constant_name
      end
      # `T.untyped` matches `T::Array[T.untyped]` so the array signature
      # must be defined first for Sorbet to pick it, if valid.
      method.add_sig do |sig|
        sig.add_param("attributes", "T::Array[T.untyped]")
        sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)")
        sig.return_type = "T::Array[#{constant_name}]"
      end
      method.add_sig do |sig|
        sig.add_param("attributes", "T.untyped")
        sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)")
        sig.return_type = constant_name
      end
    end
  end
  # We are creating `#new` on the class itself since when called as `Model.new`
  # it doesn't allow for an array to be passed. If we kept it as a blanket it
  # would mean the passing any `T.untyped` value to the method would assume
  # the result is `T::Array` which is not the case majority of the time.
  model.create_method("new", class_method: true) do |method|
    method.add_opt_param("attributes", "nil")
    method.add_block_param("block")
    method.add_sig do |sig|
      sig.add_param("attributes", "T.untyped")
      sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)")
      sig.return_type = constant_name
    end
  end
end

def create_group_chain_methods(klass)

def create_group_chain_methods(klass)
  # Calculation methods used with `group` return a hash where the keys cannot be typed
  # but the values can. Technically a `group` anywhere in the query chain produces
  # this behavior but to avoid needing to re-type every query method inside this module
  # we make a simplifying assumption that the calculation method is called immediately
  # after the group (e.g. `group().count` and not `group().where().count`). The one
  # exception is `group().having().count` which is fairly idiomatic so that gets handled
  # without breaking the chain.
  klass.create_method(
    "having",
    parameters: [
      create_rest_param("args", type: "T.untyped"),
      create_block_param("blk", type: "T.untyped"),
    ],
    return_type: "T.self_type",
  )
  klass.create_method(
    "size",
    return_type: "T::Hash[T.untyped, Integer]",
  )
  CALCULATION_METHODS.each do |method_name|
    case method_name
    when :average, :maximum, :minimum
      klass.create_method(
        method_name.to_s,
        parameters: [
          create_param("column_name", type: "T.any(String, Symbol)"),
        ],
        return_type: "T::Hash[T.untyped, " \
          "#{method_name == :average ? "T.any(Integer, Float, BigDecimal)" : "T.untyped"}]",
      )
    when :calculate
      klass.create_method(
        "calculate",
        parameters: [
          create_param("operation", type: "Symbol"),
          create_param("column_name", type: "T.any(String, Symbol)"),
        ],
        return_type: "T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]",
      )
    when :count
      klass.create_method(
        "count",
        parameters: [
          create_opt_param("column_name", type: "T.untyped", default: "nil"),
        ],
        return_type: "T::Hash[T.untyped, Integer]",
      )
    when :sum
      klass.create_method(
        "sum",
        parameters: [
          create_opt_param("column_name", type: "T.nilable(T.any(String, Symbol))", default: "nil"),
          create_block_param("block", type: "T.nilable(T.proc.params(record: T.untyped).returns(T.untyped))"),
        ],
        return_type: "T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]",
      )
    end
  end
end

def create_relation_class

def create_relation_class
  superclass = "::ActiveRecord::Relation"
  # The relation subclass includes the generated relation module
  model.create_class(RelationClassName, superclass_name: superclass) do |klass|
    klass.create_include(CommonRelationMethodsModuleName)
    klass.create_include(RelationMethodsModuleName)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
    TO_ARRAY_METHODS.each do |method_name|
      klass.create_method(method_name.to_s, return_type: "T::Array[#{constant_name}]")
    end
  end
  create_relation_group_chain_class
  create_relation_where_chain_class
end

def create_relation_group_chain_class

def create_relation_group_chain_class
  model.create_class(RelationGroupChainClassName, superclass_name: RelationClassName) do |klass|
    create_group_chain_methods(klass)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
  end
end

def create_relation_method(

def create_relation_method(
  name,
  parameters: [],
  relation_return_type: RelationClassName,
  association_return_type: AssociationRelationClassName
)
  relation_methods_module.create_method(
    name.to_s,
    parameters: parameters,
    return_type: relation_return_type,
  )
  association_relation_methods_module.create_method(
    name.to_s,
    parameters: parameters,
    return_type: association_return_type,
  )
end

def create_relation_methods

def create_relation_methods
  create_relation_method("all")
  QUERY_METHODS.each do |method_name|
    case method_name
    when :where
      create_where_relation_method
    when :group
      create_relation_method(
        "group",
        parameters: [
          create_rest_param("args", type: "T.untyped"),
          create_block_param("blk", type: "T.untyped"),
        ],
        relation_return_type: RelationGroupChainClassName,
        association_return_type: AssociationRelationGroupChainClassName,
      )
    when :distinct
      create_relation_method(
        method_name.to_s,
        parameters: [create_opt_param("value", type: "T::Boolean", default: "true")],
      )
    when :extract_associated
      parameters = [create_param("association", type: "Symbol")]
      return_type = "T::Array[T.untyped]"
      relation_methods_module.create_method(
        method_name.to_s,
        parameters: parameters,
        return_type: return_type,
      )
      association_relation_methods_module.create_method(
        method_name.to_s,
        parameters: parameters,
        return_type: return_type,
      )
    when :select
      [relation_methods_module, association_relation_methods_module].each do |mod|
        mod.create_method(method_name.to_s) do |method|
          method.add_rest_param("args")
          method.add_block_param("blk")
          method.add_sig do |sig|
            sig.add_param("args", "T.untyped")
            sig.return_type = mod == relation_methods_module ? RelationClassName : AssociationRelationClassName
          end
          method.add_sig do |sig|
            sig.add_param("blk", "T.proc.params(record: #{constant_name}).returns(BasicObject)")
            sig.return_type = "T::Array[#{constant_name}]"
          end
        end
      end
    else
      create_relation_method(
        method_name,
        parameters: [
          create_rest_param("args", type: "T.untyped"),
          create_block_param("blk", type: "T.untyped"),
        ],
      )
    end
  end
end

def create_relation_where_chain_class

def create_relation_where_chain_class
  model.create_class(RelationWhereChainClassName) do |klass|
    create_where_chain_methods(klass, RelationClassName)
    klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
  end
end

def create_where_chain_methods(klass, return_type)

def create_where_chain_methods(klass, return_type)
  WHERE_CHAIN_QUERY_METHODS.each do |method_name|
    case method_name
    when :not
      klass.create_method(
        method_name.to_s,
        parameters: [
          create_param("opts", type: "T.untyped"),
          create_rest_param("rest", type: "T.untyped"),
        ],
        return_type: return_type,
      )
    when :associated, :missing
      klass.create_method(
        method_name.to_s,
        parameters: [
          create_rest_param("args", type: "T.untyped"),
        ],
        return_type: return_type,
      )
    end
  end
end

def create_where_relation_method

def create_where_relation_method
  relation_methods_module.create_method("where") do |method|
    method.add_rest_param("args")
    method.add_sig do |sig|
      sig.return_type = RelationWhereChainClassName
    end
    method.add_sig do |sig|
      sig.add_param("args", "T.untyped")
      sig.return_type = RelationClassName
    end
  end
  association_relation_methods_module.create_method("where") do |method|
    method.add_rest_param("args")
    method.add_sig do |sig|
      sig.return_type = AssociationRelationWhereChainClassName
    end
    method.add_sig do |sig|
      sig.add_param("args", "T.untyped")
      sig.return_type = AssociationRelationClassName
    end
  end
end

def decorate

def decorate
  create_classes_and_includes
  create_common_methods
  create_relation_methods
  create_association_relation_methods
end

def gather_constants

def gather_constants
  ActiveRecord::Base.descendants.reject(&:abstract_class?)
end

def model

def model
  @model ||= T.let(
    root.create_path(constant),
    T.nilable(RBI::Scope),
  )
end

def relation_methods_module

def relation_methods_module
  @relation_methods_module ||= T.let(
    model.create_module(RelationMethodsModuleName),
    T.nilable(RBI::Scope),
  )
end