Class: Automodel::SchemaInspector

Inherits:
Object
  • Object
show all
Defined in:
lib/automodel/schema_inspector.rb

Overview

A utility object that issues the actual database inspection commands and returns the table, column, primary-key, and foreign-key data.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_handler) ⇒ SchemaInspector

Returns a new instance of SchemaInspector

Parameters:

  • connection_handler (ActiveRecord::ConnectionHandling)

    The connection pool/handler (an object that implements ActiveRecord::ConnectionHandling) to inspect and map out.



64
65
66
67
68
69
# File 'lib/automodel/schema_inspector.rb', line 64

def initialize(connection_handler)
  @connection = connection_handler.connection
  adapter = connection_handler.connection_pool.spec.config[:adapter]

  @registration = known_adapters[adapter.to_sym] || {}
end

Class Method Details

.known_adaptersObject



18
# File 'lib/automodel/schema_inspector.rb', line 18

def self.known_adapters; @known_adapters; end

.register_adapter(adapter:, tables:, columns:, primary_key:, foreign_keys: nil) ⇒ Object

“Registers” an adapter with the Automodel::SchemaInspector. This allows for alternate mechanisms of procuring lists of tables, columns, primary keys, and/or foreign keys from an adapter that may not itself support #tables/#columns/#primary_key/#foreign_keys.

Parameters:

  • adapter (String, Symbol)

    The “adapter” value used to match that given in the connection spec. It is with this value that the adapter being registered is matched to an existing database pool/connection.

  • tables (Proc)

    The Proc to #call to request a list of table names. The Proc will be called with one parameter: a database connection.

  • columns (Proc)

    The Proc to #call to request a list of columns for a specific table. The Proc will be called with two parameters: a database connection and a table name.

  • primary_key (Proc)

    The Proc to #call to request the primary key for a specific table. The Proc will be called with two parameters: a database connection and a table name.

  • foreign_keys (Proc)

    The Proc to #call to request a list of foreign keys for a specific table. The Proc will be called with two parameters: a database connection and a table name.

Raises:



50
51
52
53
54
55
56
57
58
# File 'lib/automodel/schema_inspector.rb', line 50

def self.register_adapter(adapter:, tables:, columns:, primary_key:, foreign_keys: nil)
  adapter = adapter.to_sym.downcase
  raise Automodel::AdapterAlreadyRegisteredError, adapter if known_adapters.key? adapter

  known_adapters[adapter] = { tables: tables,
                              columns: columns,
                              primary_key: primary_key,
                              foreign_keys: foreign_keys }
end

Instance Method Details

#columns(table_name) ⇒ Array<ActiveRecord::ConnectionAdapters::Column>

Returns a list of columns for the given table.

If a matching Automodel::SchemaInspector registration is found for the connection's adapter, and that registration specified a :columns Proc, the Proc is called. Otherwise, the standard connection #columns is returned.

Parameters:

  • table_name (String)

    The table whose columns should be fetched.

Returns:

  • (Array<ActiveRecord::ConnectionAdapters::Column>)


101
102
103
104
105
106
107
108
109
110
# File 'lib/automodel/schema_inspector.rb', line 101

def columns(table_name)
  table_name = table_name.to_s

  @columns ||= {}
  @columns[table_name] ||= if @registration[:columns].present?
                             @registration[:columns].call(@connection, table_name)
                           else
                             @connection.columns(table_name)
                           end
end

#foreign_keys(table_name) ⇒ Array<ActiveRecord::ConnectionAdapters::ForeignKeyDefinition>

Returns a list of foreign keys for the given table.

If a matching Automodel::SchemaInspector registration is found for the connection's adapter, and that registration specified a :foreign_keys Proc, the Proc is called. Otherwise, the standard connection #foreign_keys is attempted. If that call to `#foreign_keys raises a ::NoMethodError or ::NotImplementedError, a best-effort attempt is made to build a list of foreign keys based on table and column names.

Parameters:

  • table_name (String)

    The table whose foreign keys should be fetched.

Returns:

  • (Array<ActiveRecord::ConnectionAdapters::ForeignKeyDefinition>)


151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/automodel/schema_inspector.rb', line 151

def foreign_keys(table_name)
  table_name = table_name.to_s

  @foreign_keys ||= {}
  @foreign_keys[table_name] ||= begin
    if @registration[:foreign_keys].present?
      @registration[:foreign_keys].call(@connection, table_name)
    else
      begin
        @connection.foreign_keys(table_name)
      rescue ::NoMethodError, ::NotImplementedError
        ## Not all ActiveRecord adapters support `#foreign_keys`. When this happens, we'll make
        ## a best-effort attempt to intuit relationships from the table and column names.
        ##
        columns(table_name).map do |column|
          id_pattern = %r{(?:_id|Id)$}
          next unless column.name =~ id_pattern

          target_table = column.name.sub(id_pattern, '')
          next unless target_table.in? tables

          target_column = primary_key(qualified_name(target_table, context: table_name))
          next unless target_column.in? ['id', 'Id', 'ID', column.name]

          ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
            table_name.split('.').last,
            target_table,
            name: "FK_#{SecureRandom.uuid.delete('-')}",
            column:  column.name,
            primary_key: target_column,
            on_update: nil,
            on_delete: nil
          )
        end.compact
      end
    end
  end
end

#known_adaptersObject



19
# File 'lib/automodel/schema_inspector.rb', line 19

def known_adapters; self.class.known_adapters; end

#primary_key(table_name) ⇒ String+

Returns the primary key for the given table.

If a matching Automodel::SchemaInspector registration is found for the connection's adapter, and that registration specified a :primary_key Proc, the Proc is called. Otherwise, the standard connection #primary_key is returned.

Parameters:

  • table_name (String)

    The table whose primary key should be fetched.

Returns:

  • (String, Array<String>)


125
126
127
128
129
130
131
132
133
134
# File 'lib/automodel/schema_inspector.rb', line 125

def primary_key(table_name)
  table_name = table_name.to_s

  @primary_keys ||= {}
  @primary_keys[table_name] ||= if @registration[:primary_key].present?
                                  @registration[:primary_key].call(@connection, table_name)
                                else
                                  @connection.primary_key(table_name)
                                end
end

#tablesArray<String>

Returns a list of table names in the target database.

If a matching Automodel::SchemaInspector registration is found for the connection's adapter, and that registration specified a :tables Proc, the Proc is called. Otherwise, the standard connection #tables is returned.

Returns:

  • (Array<String>)


80
81
82
83
84
85
86
# File 'lib/automodel/schema_inspector.rb', line 80

def tables
  @tables ||= if @registration[:tables].present?
                @registration[:tables].call(@connection)
              else
                @connection.tables
              end
end