class Object

Public Instance Methods

automodel(spec) click to toggle source

The main (really only) entrypoint for the automodel-sqlserver gem. This is the method the end-user calls to trigger a database scrape and model generation.

@param spec [Symbol, String, Hash]

The Symbol/String/Hash to pass through to the ActiveRecord connection resolver, as detailed in
[ActiveRecord::ConnectionHandling#establish_connection](http://bit.ly/2JQdA8c). Whether the
given "spec" value is a Hash or is a Symbol/String to run through the ActiveRecord resolver,
the resulting Hash may include the following options (in addition to the actual connection
parameters).

@option spec [String] :subschema

The name of an additional namespace with which tables in the target database are prefixed.
Intended for use with SQL Server, where a table's fully-qualified name may have an additional
level of namespacing between the database name and the base table name (e.g.
`database.dbo.table`, in which case the subschema would be `"dbo"`).

@option spec [String] :namespace

A String representing the desired namespace for the generated model classes (e.g. `"NewDB"` or
`"WeirdDB::Models"`). If not given, the generated models will fall under `Kernel` so they are
always available without namespacing, like standard user-defined model classes.

@raise [Automodel::ModelNameCollisionError]

@return [ActiveRecord::Base]

The returned value is an instance of an ActiveRecord::Base subclass. This is the class that
serves as superclass to all of the generated model classes, so that a list of all models can be
easily compiled by calling `#subclasses` on this value.
# File lib/automodel.rb, line 40
def automodel(spec)
  ## Build out a connection spec Hash from the given value.
  ##
  resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver
  connection_spec = resolver.new(ActiveRecord::Base.configurations).resolve(spec).symbolize_keys

  ## We need a base class for all of the models we're about to create, but don't want to pollute
  ## ActiveRecord::Base's own connection pool, so we'll need a subclass. This will serve as both
  ## our base class for new models and as the connection pool handler. We're defining it with names
  ## that reflect both uses just to keep the code more legible.
  ##
  connection_handler_name = "CH_#{SecureRandom.uuid.delete('-')}"
  base_class_for_new_models = connection_handler = Class.new(ActiveRecord::Base)
  Automodel::Helpers.register_class(connection_handler, as: connection_handler_name,
                                                        within: :'Automodel::Connectors')

  ## Establish a connection with the given params.
  ##
  connection_handler.establish_connection(connection_spec)

  ## Map out the table structures.
  ##
  tables = Automodel::Helpers.map_tables(connection_handler, subschema: connection_spec[:subschema])

  ## Safeguard against class name collisions.
  ##
  defined_names = Array((connection_spec[:namespace] || :Kernel).to_s.safe_constantize&.constants)
  potential_names = tables.map { |table| table[:model_name].to_sym }
  name_collisions = defined_names & potential_names
  if name_collisions.present?
    connection_handler.connection_pool.disconnect!
    Automodel::Connectors.send(:remove_const, connection_handler_name)
    raise Automodel::NameCollisionError, name_collisions
  end

  ## Define the table models.
  ##
  tables.each do |table|
    table[:model] = Class.new(base_class_for_new_models) do
      ## We can't assume table properties confom to any standard.
      ##
      self.table_name = table[:name]
      self.primary_key = table[:primary_key]

      ## Don't allow `#find` for tables with a composite primary key.
      ##
      def find(*args)
        raise Automodel::FindOnCompoundPrimaryKeyError if table[:composite_primary_key]
        super
      end

      ## Create railsy column name aliases whenever possible.
      ##
      table[:columns].each do |column|
        railsy_name = Automodel::Helpers.railsy_column_name(column)
        unless table[:column_aliases].key? railsy_name
          table[:column_aliases][railsy_name] = column
          alias_attribute(railsy_name, column.name)
        end
      end
    end

    ## Register the model class.
    ##
    Automodel::Helpers.register_class(table[:model], as: table[:model_name],
                                                     within: connection_spec[:namespace])
  end

  ## With all models registered, we can safely declare relationships.
  ##
  tables.map { |table| table[:foreign_keys] }.flatten.each do |fk|
    from_table = tables.find { |table| table[:base_name] == fk.from_table.delete('"') }
    next unless from_table.present?

    to_table = tables.find { |table| table[:base_name] == fk.to_table.delete('"') }
    next unless to_table.present?

    association_setup = <<~END_OF_HEREDOC
      belongs_to #{to_table[:base_name].to_sym.inspect},
                 class_name: #{to_table[:model].to_s.inspect},
                 primary_key: #{fk.options[:primary_key].to_sym.inspect},
                 foreign_key: #{fk.options[:column].to_sym.inspect}

      alias #{to_table[:model_name].underscore.to_sym.inspect} #{to_table[:base_name].to_sym.inspect}
    END_OF_HEREDOC
    from_table[:model].class_eval(association_setup, __FILE__, __LINE__)
  end

  ## There's no obvious value we can return that would be of any use, except maybe the base class,
  ## in case the end user wants to procure a list of all the models (via `#subclasses`).
  ##
  base_class_for_new_models
end
find(*args) click to toggle source

Don't allow `#find` for tables with a composite primary key.

Calls superclass method
# File lib/automodel.rb, line 86
def find(*args)
  raise Automodel::FindOnCompoundPrimaryKeyError if table[:composite_primary_key]
  super
end