module Hypershield

Constants

VERSION

Attributes

enabled[RW]
log_sql[RW]
schemas[RW]

Public Class Methods

drop_view(view) click to toggle source
# File lib/hypershield.rb, line 25
def drop_view(view)
  schemas.each do |schema, _|
    execute("DROP VIEW IF EXISTS #{quote_ident(schema)}.#{quote_ident(view)}")
  end
end
refresh(dry_run: false) click to toggle source
# File lib/hypershield.rb, line 31
def refresh(dry_run: false)
  if adapter_name =~ /sqlite/i
    raise "Adapter not supported: #{adapter_name}"
  end

  quiet_logging do
    statements = []

    schemas.each do |schema, config|
      hide = config[:hide].to_a
      show = config[:show].to_a

      hypershield_tables = tables(schema)

      tables.sort_by { |k, _| k }.each do |table, columns|
        next if table == "pg_stat_statements"

        columns.reject! do |column|
          hide.any? { |m| "#{table}.#{column}".include?(m) } &&
            !show.any? { |m| "#{table}.#{column}".include?(m) }
        end

        # if the hypershield view has the same columns, assume it doesn't need updated
        # this may not necessarily be true if someone manually updates the view
        # we could check the view definition, but this is harder as the database normalizes it
        next if hypershield_tables[table] == columns

        statements << "DROP VIEW IF EXISTS #{quote_ident(schema)}.#{quote_ident(table)} CASCADE"

        if columns.any?
          statements << "CREATE VIEW #{quote_ident(schema)}.#{quote_ident(table)} AS SELECT #{columns.map { |c| quote_ident(c) }.join(", ")} FROM #{quote_ident(table)}"
        end
      end
    end

    if dry_run
      if statements.any?
        puts statements.map { |v| "#{v};" }.join("\n")
      end
    else
      # originally this was performed in a transaction
      # however, this appears to cause issues in certain situations - see #5 and #6
      # this shouldn't be a huge issue now that we only update specific views
      # we already drop views outside of the transaction when migrations are run
      statements.each do |statement|
        execute(statement)
      end
    end
  end
end

Private Class Methods

adapter_name() click to toggle source
# File lib/hypershield.rb, line 102
def adapter_name
  connection.adapter_name
end
connection() click to toggle source
# File lib/hypershield.rb, line 98
def connection
  ActiveRecord::Base.connection
end
execute(sql) click to toggle source
# File lib/hypershield.rb, line 145
def execute(sql)
  connection.execute(sql)
end
mysql?() click to toggle source
# File lib/hypershield.rb, line 106
def mysql?
  adapter_name =~ /mysql/i
end
quiet_logging() { || ... } click to toggle source
# File lib/hypershield.rb, line 84
def quiet_logging
  if ActiveRecord::Base.logger && !log_sql
    previous_level = ActiveRecord::Base.logger.level
    begin
      ActiveRecord::Base.logger.level = Logger::INFO
      yield
    ensure
      ActiveRecord::Base.logger.level = previous_level
    end
  else
    yield
  end
end
quote(literal) click to toggle source
# File lib/hypershield.rb, line 149
def quote(literal)
  connection.quote(literal)
end
quote_ident(ident) click to toggle source
# File lib/hypershield.rb, line 153
def quote_ident(ident)
  connection.quote_table_name(ident.to_s)
end
select_all(sql) click to toggle source
# File lib/hypershield.rb, line 141
def select_all(sql)
  connection.select_all(sql).to_a
end
tables(schema = nil) click to toggle source
# File lib/hypershield.rb, line 110
    def tables(schema = nil)
      if schema
        schema = quote(schema)
      else
        schema =
          if mysql?
            "database()"
          else
            "'public'"
          end
      end

      query = <<-SQL
        SELECT
          table_name,
          column_name,
          ordinal_position,
          data_type
        FROM
          information_schema.columns
        WHERE
          table_schema = #{schema}
      SQL

      select_all(query.squish)
        .map { |c| c.transform_keys(&:downcase) }
        .group_by { |c| c["table_name"] }
        .map { |t, cs| [t, cs.sort_by { |c| c["ordinal_position"].to_i }.map { |c| c["column_name"] }] }
        .to_h
    end