moertel/sQucumber-redshift

View on GitHub
lib/squcumber-redshift/mock/database.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'pg'

module Squcumber
  module Redshift
    module Mock
      class Database
        DELETE_DB_WHEN_FINISHED = ENV['KEEP_TEST_DB'].to_i == 1 ? false : true
        TEST_DB_NAME_OVERRIDE = ENV.fetch('TEST_DB_NAME_OVERRIDE', '')

        def initialize(production_database)
          @production_database = production_database or raise ArgumentError, 'No production database provided'

          test_db_name_postfix = TEST_DB_NAME_OVERRIDE.empty? ? rand(10000..99999) : TEST_DB_NAME_OVERRIDE
          @test_db_name = "test_env_#{test_db_name_postfix}"

          if @production_database.exec("select datname from pg_database where datname like '%#{@test_db_name}%'").num_tuples != 0
            @production_database.exec("drop database #{@test_db_name}")
          end
          @production_database.exec("create database #{@test_db_name}")

          @testing_database = PG.connect(
            host: ENV['REDSHIFT_HOST'],
            port: ENV['REDSHIFT_PORT'],
            dbname: @test_db_name,
            user: ENV['REDSHIFT_USER'],
            password: ENV['REDSHIFT_PASSWORD']
          )
        end

        def setup(schemas)
          schemas.each do |schema|
            exec("drop schema if exists #{schema} cascade")
            exec("create schema #{schema}")
          end
        end

        def truncate_all_tables
          @testing_database
            .exec("select schemaname || '.' || tablename as schema_and_table from pg_tables where tableowner = '#{ENV['REDSHIFT_USER']}'")
            .map { |row| row['schema_and_table'] }
            .each { |schema_and_table| exec("truncate table #{schema_and_table}") }
        end

        def exec(statement)
          @testing_database.exec(statement)
        end
        alias_method :query, :exec

        def exec_file(path)
          exec(File.read("#{path}"))
        end
        alias_method :query_file, :exec_file

        # Redshift does not allow to copy a table schema to another database, i.e.
        # `create table some_db.some_table (like another_db.some_table)` cannot be used.
        def copy_table_def_from_prod(schema, table)
          create_table_statement = _get_create_table_statement(schema, table)
          exec(create_table_statement)
        end

        def copy_table_defs_from_prod(tables)
          tables.each do |obj|
            obj.each { |schema, table| copy_table_def_from_prod(schema, table) }
          end
        end

        def mock(mock)
          mock.each do |schema_and_table, data|
            raise "Mock data for #{schema_and_table} is not correctly formatted: must be Array but was #{data.class}" unless data.is_a?(Array)
            data.each { |datum| insert_mock_values(schema_and_table, datum) }
          end
        end

        def insert_mock_values(schema_and_table, mock)
          schema, table = schema_and_table.split('.')
          keys = []
          vals = []
          mock.each do |key, value|
            unless value.nil?
              keys << key
              vals << (value.is_a?(String) ? "'#{value}'" : value)
            end
          end
          exec("insert into #{schema}.#{table} (#{keys.join(',')}) values (#{vals.join(',')})") unless vals.empty?
        end

        def destroy
          @testing_database.close()

          if DELETE_DB_WHEN_FINISHED
            attempts = 0
            begin
              attempts += 1
              @production_database.exec("drop database #{@test_db_name}")
            rescue PG::ObjectInUse
              sleep 5
              retry unless attempts >= 3
            end
          else
            puts "\nTest database has been kept alive: #{@test_db_name}"
          end

          @production_database.close()
        end

        private

        def _get_create_table_statement(schema, table)
          @production_database.exec("set search_path to '$user', #{schema};")
          table_schema = @production_database.query("select * from pg_table_def where schemaname = '#{schema}' and tablename = '#{table}';")
          raise "Sorry, there is no table information for #{schema}.#{table}" if table_schema.num_tuples == 0

          distkey = _get_table_distkey(table_schema)
          sortkeys = _get_table_sortkeys(table_schema).join(',')
          definitions = _get_column_definitions(table_schema).join(',')

          table_distkey  = "distkey(#{distkey})"            unless distkey.nil?
          table_sortkeys = "sortkey(#{sortkeys})" unless sortkeys.empty?

          "create table if not exists #{schema}.#{table} (#{definitions}) #{table_distkey} #{table_sortkeys};"
        end

        def _get_table_distkey(table_definition)
          table_definition.select { |definition| definition['distkey'].eql?('t') }[0]['column'] rescue nil
        end

        def _get_table_sortkeys(table_definition)
          table_definition.sort_by { |e| e['sortkey'].to_i }.select { |e| e['sortkey'].to_i != 0 }.map { |e| e['column'] } rescue nil
        end

        def _get_column_definitions(table_definition)
          table_definition.map { |definition| "#{definition['column']} #{definition['type']} default null" }
        end
      end
    end
  end
end