module Optic::Rails

Constants

VERSION

Public Class Methods

entities(logger) click to toggle source
# File lib/optic/rails.rb, line 8
def entities(logger)
  with_connection do
    {
      schema_version: ActiveRecord::Migrator.current_version,
      entities: active_record_klasses.map do |klass|
        begin
          logger.debug "Inspecting entity #{klass}"
          {
            name: klass.name,
            table_name: klass.table_name,
            entity_attribute_names: klass.attribute_names,
            table_exists: klass.table_exists?,
            associations: klass.reflect_on_all_associations.map do |reflection|
              begin
                logger.debug "Inspecting association #{klass}/#{reflection.name}"
                {
                  name: reflection.name,
                  macro: reflection.macro,
                  options: reflection.options.map { |k, v| [k, v.to_s] }.to_h,
                  klass_name: reflection.options[:polymorphic] ? nil : reflection.klass.name,
                }
              rescue => e
                logger.warn "Could not inspect association, error was: #{e}"
                nil
              end
            end.compact
          }
        rescue => e
          logger.warn "Could not inspect entity, error was: #{e}"
          nil
        end
      end.compact
    }
  end
end
metrics(logger, instructions) click to toggle source
# File lib/optic/rails.rb, line 44
      def metrics(logger, instructions)
        with_connection do |connection|
          instructions.map do |instruction|
            logger.debug "Gathering metrics for metric_configuration_id = #{instruction['metric_configuration_id']}"
            name = instruction["entity"]
            entity = entity_for_entity_name name

            query =
              if pivot_name = instruction["pivot"]
                # TODO this is a terrible hack to extract zero-valued pivot
                # instances. The right thing to do is select from the pivot
                # table and LEFT OUTER JOIN to the entity table, which results
                # in a much simpler query, but it means we have to rewrite the
                # pathfinding logic in the server to find paths using has_many
                # associations instead of belongs_to associations, which might
                # be less accurate for a bunch of reasons. For now, we're doing
                # an INNER JOIN selecting from the entity table and then
                # selecting every possible pivot value as a UNION, throwing out
                # the duplicates.
                pivot = entity_for_entity_name pivot_name
                join_path = instruction["join_path"]
                joins = join_path.reverse.map(&:to_sym).inject { |acc, elt| { elt => acc } }

                columns = [
                  %Q|#{qualified_primary_key(connection, pivot)} AS primary_key|,
                  %Q|#{qualified_entity_column(connection, pivot, instruction["pivot_attribute_name"])} AS pivot_attribute_name|,
                ]

                join_select = entity
                              .joins(joins)
                              .group(qualified_primary_key(connection, pivot))
                              .select(*columns, 'COUNT(*) AS count')
                              .to_sql

                instance_select = pivot
                                  .select(*columns, '0 AS count')
                                  .to_sql

                union_sql = <<~"SQL"
                              SELECT pivot_values.primary_key, pivot_values.pivot_attribute_name, MAX(pivot_values.count) AS count
                                FROM (#{join_select} UNION ALL #{instance_select}) AS pivot_values
                                GROUP BY pivot_values.primary_key, pivot_values.pivot_attribute_name
                            SQL
              else
                entity.select('COUNT(*) as count').to_sql
              end

            { metric_configuration_id: instruction["metric_configuration_id"], result: connection.exec_query(query).to_a }
          end
        end
      end

Private Class Methods

active_record_klasses() click to toggle source
# File lib/optic/rails.rb, line 132
def active_record_klasses
  base_klass = ApplicationRecord rescue ActiveRecord::Base
  ObjectSpace.each_object(Class).find_all { |klass| klass < base_klass }
end
entity_for_entity_name(entity_name) click to toggle source
# File lib/optic/rails.rb, line 116
def entity_for_entity_name(entity_name)
  entity_name.constantize.unscoped
end
qualified_entity_column(connection, entity, attribute) click to toggle source
# File lib/optic/rails.rb, line 124
def qualified_entity_column(connection, entity, attribute)
  qualified_table_column(connection, entity.table_name, attribute)
end
qualified_primary_key(connection, entity) click to toggle source
# File lib/optic/rails.rb, line 120
def qualified_primary_key(connection, entity)
  qualified_entity_column(connection, entity, entity.primary_key)
end
qualified_table_column(connection, table_name, column_name) click to toggle source
# File lib/optic/rails.rb, line 128
def qualified_table_column(connection, table_name, column_name)
  connection.quote_table_name(table_name) + "." + connection.quote_column_name(column_name)
end
with_connection() { |connection| ... } click to toggle source

Try to be defensive with our DB connection: 1) Check a connection out from the thread pool instead of using an implicit one 2) Make the connection read-only 3) Time out any queries that take more than 100ms

# File lib/optic/rails.rb, line 102
def with_connection
  ActiveRecord::Base.connection_pool.with_connection do |connection|
    connection.transaction do
      if connection.adapter_name == "PostgreSQL"
        connection.execute "SET TRANSACTION READ ONLY"
        connection.execute "SET LOCAL statement_timeout = 100"
        # TODO support equivalent options for other adapters (such as mysql)
      end

      yield connection
    end
  end
end