flare-up 0.8 → 0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/.gitignore +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +59 -5
- data/flare-up.gemspec +2 -2
- data/lib/flare_up.rb +4 -1
- data/lib/flare_up/boot.rb +11 -11
- data/lib/flare_up/cli.rb +84 -29
- data/lib/flare_up/command/base.rb +22 -0
- data/lib/flare_up/command/copy.rb +63 -0
- data/lib/flare_up/command/create_table.rb +58 -0
- data/lib/flare_up/command/drop_table.rb +28 -0
- data/lib/flare_up/version.rb +1 -1
- data/resources/load_hearthstone_cards.rb +1 -1
- data/spec/lib/flare_up/boot_spec.rb +15 -14
- data/spec/lib/flare_up/{copy_command_spec.rb → command/copy_spec.rb} +3 -7
- data/spec/lib/flare_up/command/create_table_spec.rb +117 -0
- data/spec/lib/flare_up/command/drop_table_spec.rb +74 -0
- metadata +16 -7
- data/lib/flare_up/copy_command.rb +0 -73
    
        checksums.yaml
    CHANGED
    
    | @@ -1,15 +1,15 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            !binary "U0hBMQ==":
         | 
| 3 3 | 
             
              metadata.gz: !binary |-
         | 
| 4 | 
            -
                 | 
| 4 | 
            +
                MWI2ZDkyYjVjOTBlMWMyYjI2NzM3NjNhNThkYzFlM2Q5OWYxNDQyNw==
         | 
| 5 5 | 
             
              data.tar.gz: !binary |-
         | 
| 6 | 
            -
                 | 
| 6 | 
            +
                OTU0NTBiOGExMTA4MjhjOWViNzllMjUwMWU3YjU5NTcyODVlMmFhZA==
         | 
| 7 7 | 
             
            SHA512:
         | 
| 8 8 | 
             
              metadata.gz: !binary |-
         | 
| 9 | 
            -
                 | 
| 10 | 
            -
                 | 
| 11 | 
            -
                 | 
| 9 | 
            +
                MTM0MDVlMzAxOGUxOGQzNTRkNTQwMDBmNTQ4ZmU2MjY0OTRmZTI5YjY3OTZi
         | 
| 10 | 
            +
                ZTBiYTUxNjk5YjZiYTI5ZDRkYjI5NDljOWY0YmI2NjBkYmZiMjFkZDgyZWVl
         | 
| 11 | 
            +
                YmNmMjZjOWM0OWZmNmE0ZjZjN2E4OTlhZTNkOGFjY2E3MDY5MWY=
         | 
| 12 12 | 
             
              data.tar.gz: !binary |-
         | 
| 13 | 
            -
                 | 
| 14 | 
            -
                 | 
| 15 | 
            -
                 | 
| 13 | 
            +
                OWJkODg5ZmMyOGRjNDljZGIwNmE3MDdlMWMyMjUzZTBhY2Q5YjQ4MDNiN2Iw
         | 
| 14 | 
            +
                MTUxZWVjZTE4YmIyZDY4ZjU1ZGQ2ZjEzZTgzMGFlYmU4OTY3NmIyZGVhNWNj
         | 
| 15 | 
            +
                YmNkMDlhZmNhMzAzMzg1NmZjZjM4MWIyOGI1OTk1MDY1MGFmMDE=
         | 
    
        data/.gitignore
    CHANGED
    
    
    
        data/Gemfile.lock
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -1,11 +1,11 @@ | |
| 1 1 | 
             
            ## Overview
         | 
| 2 | 
            -
            ```Flare-up``` provides a wrapper around the Redshift [```COPY```](http://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html)  | 
| 2 | 
            +
            ```Flare-up``` provides a wrapper around the Redshift commands [```CREATE TABLE```](http://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html), [```COPY```](http://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html), and [```DROP TABLE```](http://docs.aws.amazon.com/redshift/latest/dg/r_DROP_TABLE.html)  for scriptability, allowing you to issue the commands directly from the CLI.  Much of the code is concerned with simplifying constructing the COPY command and providing easy access to the errors that may result from import.
         | 
| 3 3 |  | 
| 4 4 | 
             
            ## Why?
         | 
| 5 5 |  | 
| 6 6 | 
             
            Redshift prefers a bulk COPY operation over indidivual INSERTs which Redshift is not optimized for, and Amazon does not recommend it as a strategy for loading.  COPY is a SQL command, not something issued via the AWS Redshift REST API, meaning you need a SQL connection to your Redshift instance to bulk load data.
         | 
| 7 7 |  | 
| 8 | 
            -
            The astute consumer of the AWS toolchain will note that [Data Pipeline](http://aws.amazon.com/datapipeline/) is one way this import may be completed however, we use Azkaban and the only thing worse  | 
| 8 | 
            +
            The astute consumer of the AWS toolchain will note that [Data Pipeline](http://aws.amazon.com/datapipeline/) is one way this import may be completed however, we use Azkaban and the only thing worse than one job flow control tool is two job flow control tools :)
         | 
| 9 9 |  | 
| 10 10 | 
             
            Additionally, access to COPY errors is a bit cumbersome.  On failure, Redshift populates the ```stl_load_errors``` table which inherently must be accessed via SQL.  Flare-up will pretty print any errors that occur during import so that you may examine your logs rather than establishing a connection to Redshift to understand what went wrong.
         | 
| 11 11 |  | 
| @@ -19,10 +19,12 @@ The `pg` gem is a dependency (required to issue SQL commands to Redshift) and wi | |
| 19 19 |  | 
| 20 20 | 
             
            ## Syntax
         | 
| 21 21 |  | 
| 22 | 
            -
            Available via `flare-up help copy`.
         | 
| 22 | 
            +
            Available via `flare-up help <cmd>` where `<cmd>` can be replaced with `create_table`, `copy`, or `drop_table`.
         | 
| 23 23 |  | 
| 24 24 | 
             
            While we'd prefer if everyone stored configuration variables (esp. credentials) as environment variables (re: [Twelve-Factor App](http://12factor.net/)), it can be a pain to export variables when you're testing a tool and as such, we support specifying all of these on the command-line.
         | 
| 25 25 |  | 
| 26 | 
            +
            ### COPY
         | 
| 27 | 
            +
             | 
| 26 28 | 
             
            ```
         | 
| 27 29 | 
             
            Usage:
         | 
| 28 30 | 
             
              flare-up copy DATA_SOURCE REDSHIFT_ENDPOINT DATABASE TABLE
         | 
| @@ -38,9 +40,49 @@ Options: | |
| 38 40 | 
             
                                                           # Default: true
         | 
| 39 41 | 
             
            ```
         | 
| 40 42 |  | 
| 43 | 
            +
            ### CREATE TABLE
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            ```
         | 
| 46 | 
            +
            Usage:
         | 
| 47 | 
            +
              flare-up create_table REDSHIFT_ENDPOINT DATABASE TABLE
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            Options:
         | 
| 50 | 
            +
              [--column-list=COLUMN_LIST]                  # Required. A space-separated list of columns with their data-types, enclose "IN QUOTES"
         | 
| 51 | 
            +
              [--redshift-username=REDSHIFT_USERNAME]      # Required unless ENV['REDSHIFT_USERNAME'] is set.
         | 
| 52 | 
            +
              [--redshift-password=REDSHIFT_PASSWORD]      # Required unless ENV['REDSHIFT_PASSWORD'] is set.
         | 
| 53 | 
            +
              [--colorize-output], [--no-colorize-output]  # Should Flare-up colorize its output?
         | 
| 54 | 
            +
                                                           # Default: true
         | 
| 55 | 
            +
            ```
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            ### DROP TABLE
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            ```
         | 
| 60 | 
            +
            Usage:
         | 
| 61 | 
            +
              flare-up drop_table REDSHIFT_ENDPOINT DATABASE TABLE
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            Options:
         | 
| 64 | 
            +
              [--redshift-username=REDSHIFT_USERNAME]      # Required unless ENV['REDSHIFT_USERNAME'] is set.
         | 
| 65 | 
            +
              [--redshift-password=REDSHIFT_PASSWORD]      # Required unless ENV['REDSHIFT_PASSWORD'] is set.
         | 
| 66 | 
            +
              [--colorize-output], [--no-colorize-output]  # Should Flare-up colorize its output?
         | 
| 67 | 
            +
                                                           # Default: true
         | 
| 68 | 
            +
            ```
         | 
| 69 | 
            +
             | 
| 41 70 | 
             
            ## Sample Usage
         | 
| 42 71 |  | 
| 43 | 
            -
            Note that  | 
| 72 | 
            +
            Note that these examples assume you have credentials set as environment variables.
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            ### CREATE TABLE
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            ```
         | 
| 77 | 
            +
            > flare-up                                                      \
         | 
| 78 | 
            +
                create_table                                                \
         | 
| 79 | 
            +
                flare-up-test.cskjnp4xvaje.us-west-2.redshift.amazonaws.com \
         | 
| 80 | 
            +
                dev                                                         \
         | 
| 81 | 
            +
                hearthstone_cards                                           \
         | 
| 82 | 
            +
                --column-list "id char(24) name varchar(2000)"
         | 
| 83 | 
            +
            ```
         | 
| 84 | 
            +
             | 
| 85 | 
            +
            ### COPY
         | 
| 44 86 |  | 
| 45 87 | 
             
            ```
         | 
| 46 88 | 
             
            > flare-up                                                      \
         | 
| @@ -50,5 +92,17 @@ Note that this example assumes you have credentials set as environment variables | |
| 50 92 | 
             
                dev                                                         \
         | 
| 51 93 | 
             
                hearthstone_cards                                           \
         | 
| 52 94 | 
             
                --column-list name cost attack health description           \
         | 
| 53 | 
            -
                --copy-options "REGION 'us-east-1' CSV"
         | 
| 95 | 
            +
                --copy-options "REGION 'us-east-1' CSV IGNOREHEADER 1"
         | 
| 96 | 
            +
            ```
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            - The handy `IGNOREHEADER 1` option ignores the first line of field names  in the csv file.
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            ### DROP TABLE
         | 
| 101 | 
            +
             | 
| 102 | 
            +
            ```
         | 
| 103 | 
            +
            > flare-up                                                      \
         | 
| 104 | 
            +
                drop_table                                                  \
         | 
| 105 | 
            +
                flare-up-test.cskjnp4xvaje.us-west-2.redshift.amazonaws.com \
         | 
| 106 | 
            +
                dev                                                         \
         | 
| 107 | 
            +
                hearthstone_cards
         | 
| 54 108 | 
             
            ```
         | 
    
        data/flare-up.gemspec
    CHANGED
    
    | @@ -7,8 +7,8 @@ Gem::Specification.new do |s| | |
| 7 7 | 
             
              s.platform    = Gem::Platform::RUBY
         | 
| 8 8 | 
             
              s.authors     = ['Robert Slifka']
         | 
| 9 9 | 
             
              s.homepage    = 'http://www.github.com/sharethrough/flare-up'
         | 
| 10 | 
            -
              s.summary     = %q{Command-line access to bulk data loading via Redshift's COPY  | 
| 11 | 
            -
              s.description = %q{Flare-up makes Redshift COPY scriptable by providing CLI access to the Redshift COPY command, with handy access to pretty printed errors as well.}
         | 
| 10 | 
            +
              s.summary     = %q{Command-line access to bulk data loading via Redshift's CREATE TABLE, COPY, and DROP TABLE commands.}
         | 
| 11 | 
            +
              s.description = %q{Flare-up makes Redshift COPY scriptable by providing CLI access to the Redshift COPY command, with handy access to pretty printed errors as well. It also includes the CREATE TABLE and DROP TABLE commands.}
         | 
| 12 12 |  | 
| 13 13 | 
             
              s.add_dependency('pg', '~> 0.17')
         | 
| 14 14 | 
             
              s.add_dependency('thor', '~> 0.19')
         | 
    
        data/lib/flare_up.rb
    CHANGED
    
    | @@ -11,7 +11,10 @@ require 'flare_up/emitter' | |
| 11 11 | 
             
            require 'flare_up/connection'
         | 
| 12 12 | 
             
            require 'flare_up/stl_load_error'
         | 
| 13 13 | 
             
            require 'flare_up/stl_load_error_fetcher'
         | 
| 14 | 
            -
            require 'flare_up/ | 
| 14 | 
            +
            require 'flare_up/command/base'
         | 
| 15 | 
            +
            require 'flare_up/command/copy'
         | 
| 16 | 
            +
            require 'flare_up/command/create_table'
         | 
| 17 | 
            +
            require 'flare_up/command/drop_table'
         | 
| 15 18 |  | 
| 16 19 | 
             
            require 'flare_up/boot'
         | 
| 17 20 | 
             
            require 'flare_up/cli'
         | 
    
        data/lib/flare_up/boot.rb
    CHANGED
    
    | @@ -3,9 +3,9 @@ module FlareUp | |
| 3 3 | 
             
              class Boot
         | 
| 4 4 |  | 
| 5 5 | 
             
                # TODO: This control flow is untested and too procedural
         | 
| 6 | 
            -
                def self.boot
         | 
| 6 | 
            +
                def self.boot(command)
         | 
| 7 7 | 
             
                  conn = create_connection
         | 
| 8 | 
            -
                   | 
| 8 | 
            +
                  cmd = create_command(command)
         | 
| 9 9 |  | 
| 10 10 | 
             
                  begin
         | 
| 11 11 | 
             
                    trap('SIGINT') do
         | 
| @@ -19,12 +19,12 @@ module FlareUp | |
| 19 19 | 
             
                      CLI.bailout(1)
         | 
| 20 20 | 
             
                    end
         | 
| 21 21 |  | 
| 22 | 
            -
                    Emitter.info("Executing command: #{ | 
| 23 | 
            -
                    handle_load_errors( | 
| 22 | 
            +
                    Emitter.info("Executing command: #{cmd.get_command}")
         | 
| 23 | 
            +
                    handle_load_errors(cmd.execute(conn))
         | 
| 24 24 | 
             
                  rescue ConnectionError => e
         | 
| 25 25 | 
             
                    Emitter.error(e.message)
         | 
| 26 26 | 
             
                    CLI.bailout(1)
         | 
| 27 | 
            -
                  rescue  | 
| 27 | 
            +
                  rescue CommandError => e
         | 
| 28 28 | 
             
                    Emitter.error(e.message)
         | 
| 29 29 | 
             
                    CLI.bailout(1)
         | 
| 30 30 | 
             
                  end
         | 
| @@ -41,18 +41,18 @@ module FlareUp | |
| 41 41 | 
             
                end
         | 
| 42 42 | 
             
                private_class_method :create_connection
         | 
| 43 43 |  | 
| 44 | 
            -
                def self. | 
| 45 | 
            -
                   | 
| 44 | 
            +
                def self.create_command(klass)
         | 
| 45 | 
            +
                  cmd = klass.new(
         | 
| 46 46 | 
             
                    OptionStore.get(:table),
         | 
| 47 47 | 
             
                    OptionStore.get(:data_source),
         | 
| 48 48 | 
             
                    OptionStore.get(:aws_access_key),
         | 
| 49 49 | 
             
                    OptionStore.get(:aws_secret_key)
         | 
| 50 50 | 
             
                  )
         | 
| 51 | 
            -
                   | 
| 52 | 
            -
                   | 
| 53 | 
            -
                   | 
| 51 | 
            +
                  cmd.columns = OptionStore.get(:column_list) if OptionStore.get(:column_list)
         | 
| 52 | 
            +
                  cmd.options = OptionStore.get(:copy_options) if OptionStore.get(:copy_options)
         | 
| 53 | 
            +
                  cmd
         | 
| 54 54 | 
             
                end
         | 
| 55 | 
            -
                private_class_method : | 
| 55 | 
            +
                private_class_method :create_command
         | 
| 56 56 |  | 
| 57 57 | 
             
                # TODO: Backfill tests
         | 
| 58 58 | 
             
                def self.handle_load_errors(stl_load_errors)
         | 
    
        data/lib/flare_up/cli.rb
    CHANGED
    
    | @@ -2,44 +2,99 @@ module FlareUp | |
| 2 2 |  | 
| 3 3 | 
             
              class CLI < Thor
         | 
| 4 4 |  | 
| 5 | 
            +
                ### shared methods and variables
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                no_commands {
         | 
| 8 | 
            +
                  def command_setup(data_source, endpoint, database_name, table_name)
         | 
| 9 | 
            +
                    boot_options = {
         | 
| 10 | 
            +
                      :data_source => data_source,
         | 
| 11 | 
            +
                      :redshift_endpoint => endpoint,
         | 
| 12 | 
            +
                      :database => database_name,
         | 
| 13 | 
            +
                      :table => table_name
         | 
| 14 | 
            +
                    }
         | 
| 15 | 
            +
                    options.each { |k, v| boot_options[k.to_sym] = v }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    begin
         | 
| 18 | 
            +
                      CLI.env_validator(boot_options, :aws_access_key, 'AWS_ACCESS_KEY_ID')
         | 
| 19 | 
            +
                      CLI.env_validator(boot_options, :aws_secret_key, 'AWS_SECRET_ACCESS_KEY')
         | 
| 20 | 
            +
                      CLI.env_validator(boot_options, :redshift_username, 'REDSHIFT_USERNAME')
         | 
| 21 | 
            +
                      CLI.env_validator(boot_options, :redshift_password, 'REDSHIFT_PASSWORD')
         | 
| 22 | 
            +
                    rescue ArgumentError => e
         | 
| 23 | 
            +
                      Emitter.error(e.message)
         | 
| 24 | 
            +
                      CLI.bailout(1)
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    OptionStore.store_options(boot_options)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def no_datasource_command(endpoint, database_name, table_name)
         | 
| 31 | 
            +
                    command_setup(nil, endpoint, database_name, table_name)
         | 
| 32 | 
            +
                    command = CLI.command_formatter __callee__
         | 
| 33 | 
            +
                    Boot.boot command
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                }
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                all_shared_options = [
         | 
| 38 | 
            +
                  [:redshift_username, { :type => :string, :desc => "Required unless ENV['REDSHIFT_USERNAME'] is set." }],
         | 
| 39 | 
            +
                  [:redshift_password, { :type => :string, :desc => "Required unless ENV['REDSHIFT_PASSWORD'] is set." }],
         | 
| 40 | 
            +
                  [:colorize_output, { :type => :boolean, :desc => 'Should Flare-up colorize its output?', :default => true }]
         | 
| 41 | 
            +
                ]
         | 
| 42 | 
            +
                copy_options = [
         | 
| 43 | 
            +
                  [:aws_access_key, { :type => :string, :desc => "Required unless ENV['AWS_ACCESS_KEY_ID'] is set." }],
         | 
| 44 | 
            +
                  [:aws_secret_key, { :type => :string, :desc => "Required unless ENV['AWS_SECRET_ACCESS_KEY'] is set." }],
         | 
| 45 | 
            +
                  [:copy_options, { :type => :string, :desc => "Appended to the end of the command; enclose \"IN QUOTES\"" }]
         | 
| 46 | 
            +
                ]
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                long_desc_footer = <<-LONGDESCFOOTER
         | 
| 49 | 
            +
            \nDocumentation for this version can be found at:\x5
         | 
| 50 | 
            +
            https://github.com/sharethrough/flare-up/blob/v#{FlareUp::VERSION}/README.md
         | 
| 51 | 
            +
                LONGDESCFOOTER
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                ### copy command
         | 
| 54 | 
            +
             | 
| 5 55 | 
             
                desc 'copy DATA_SOURCE REDSHIFT_ENDPOINT DATABASE TABLE', 'COPY data into REDSHIFT_ENDPOINT from DATA_SOURCE into DATABASE.TABLE'
         | 
| 6 56 | 
             
                long_desc <<-LONGDESC
         | 
| 7 57 | 
             
            `flare-up copy` executes the Redshift COPY command, loading data from\x5
         | 
| 8 58 | 
             
            DATA_SOURCE into DATABASE_NAME.TABLE_NAME at REDSHIFT_ENDPOINT.
         | 
| 9 | 
            -
             | 
| 10 | 
            -
            Documentation for this version can be found at:\x5
         | 
| 11 | 
            -
            https://github.com/sharethrough/flare-up/blob/v#{FlareUp::VERSION}/README.md
         | 
| 59 | 
            +
            #{long_desc_footer}
         | 
| 12 60 | 
             
                LONGDESC
         | 
| 13 | 
            -
                option :aws_access_key, :type => :string, :desc => "Required unless ENV['AWS_ACCESS_KEY_ID'] is set."
         | 
| 14 | 
            -
                option :aws_secret_key, :type => :string, :desc => "Required unless ENV['AWS_SECRET_ACCESS_KEY'] is set."
         | 
| 15 | 
            -
                option :redshift_username, :type => :string, :desc => "Required unless ENV['REDSHIFT_USERNAME'] is set."
         | 
| 16 | 
            -
                option :redshift_password, :type => :string, :desc => "Required unless ENV['REDSHIFT_PASSWORD'] is set."
         | 
| 17 61 | 
             
                option :column_list, :type => :array, :desc => 'A space-separated list of columns, should your DATA_SOURCE require it'
         | 
| 18 | 
            -
                 | 
| 19 | 
            -
                option :colorize_output, :type => :boolean, :desc => 'Should Flare-up colorize its output?', :default => true
         | 
| 20 | 
            -
             | 
| 62 | 
            +
                [*all_shared_options, *copy_options].each { |shared_options| method_option *shared_options }
         | 
| 21 63 | 
             
                def copy(data_source, endpoint, database_name, table_name)
         | 
| 22 | 
            -
                   | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
                  options.each { |k, v| boot_options[k.to_sym] = v }
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                  begin
         | 
| 31 | 
            -
                    CLI.env_validator(boot_options, :aws_access_key, 'AWS_ACCESS_KEY_ID')
         | 
| 32 | 
            -
                    CLI.env_validator(boot_options, :aws_secret_key, 'AWS_SECRET_ACCESS_KEY')
         | 
| 33 | 
            -
                    CLI.env_validator(boot_options, :redshift_username, 'REDSHIFT_USERNAME')
         | 
| 34 | 
            -
                    CLI.env_validator(boot_options, :redshift_password, 'REDSHIFT_PASSWORD')
         | 
| 35 | 
            -
                  rescue ArgumentError => e
         | 
| 36 | 
            -
                    Emitter.error(e.message)
         | 
| 37 | 
            -
                    CLI.bailout(1)
         | 
| 38 | 
            -
                  end
         | 
| 64 | 
            +
                  command_setup(data_source, endpoint, database_name, table_name)
         | 
| 65 | 
            +
                  command = CLI.command_formatter __method__
         | 
| 66 | 
            +
                  Boot.boot command
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                ### create_table command
         | 
| 39 70 |  | 
| 40 | 
            -
             | 
| 71 | 
            +
                desc 'create_table REDSHIFT_ENDPOINT DATABASE TABLE', 'CREATE DATABASE.TABLE in REDSHIFT_ENDPOINT'
         | 
| 72 | 
            +
                long_desc <<-LONGDESC
         | 
| 73 | 
            +
            `flare-up create_table` executes the Redshift CREATE TABLE command, creating a table named\x5
         | 
| 74 | 
            +
            TABLE in DATABASE_NAME at REDSHIFT_ENDPOINT, using the scmhema provided in --column-list.
         | 
| 75 | 
            +
            #{long_desc_footer}
         | 
| 76 | 
            +
                LONGDESC
         | 
| 77 | 
            +
                option :column_list, { :type => :string, :desc => "Required. A space-separated list of columns with their data-types, enclose \"IN QUOTES\"" }
         | 
| 78 | 
            +
                [*all_shared_options].each { |shared_options| method_option *shared_options }
         | 
| 79 | 
            +
                alias_method :create_table, :no_datasource_command
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                ### drop_table command
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                desc 'drop_table REDSHIFT_ENDPOINT DATABASE TABLE', 'DROP DATABASE.TABLE in REDSHIFT_ENDPOINT'
         | 
| 84 | 
            +
                long_desc <<-LONGDESC
         | 
| 85 | 
            +
            `flare-up drop_table` executes the Redshift DROP TABLE command, removing the table named TABLE\x5
         | 
| 86 | 
            +
            in DATABASE_NAME at REDSHIFT_ENDPOINT.
         | 
| 87 | 
            +
            #{long_desc_footer}
         | 
| 88 | 
            +
                LONGDESC
         | 
| 89 | 
            +
                all_shared_options.each { |shared_options| method_option *shared_options }
         | 
| 90 | 
            +
                alias_method :drop_table, :no_datasource_command
         | 
| 41 91 |  | 
| 42 | 
            -
             | 
| 92 | 
            +
                # transforms the symbol method names to the corresponding class under
         | 
| 93 | 
            +
                # the FlareUp::Command namespace
         | 
| 94 | 
            +
                def self.command_formatter(sym)
         | 
| 95 | 
            +
                  name = sym.to_s.split('_').map(&:capitalize).join
         | 
| 96 | 
            +
                  # Ruby 1.9.3 cannot handle const_get for nested Classes (Ruby 2.0 can).
         | 
| 97 | 
            +
                  "FlareUp::Command::#{name}".split("::").reduce(Object) { |a, e| a.const_get e }
         | 
| 43 98 | 
             
                end
         | 
| 44 99 |  | 
| 45 100 | 
             
                def self.env_validator(options, option_name, env_variable_name)
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module FlareUp
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              class CommandError < StandardError
         | 
| 4 | 
            +
              end
         | 
| 5 | 
            +
              class DataSourceError < CommandError
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
              class OtherZoneBucketError < CommandError
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
              class SyntaxError < CommandError
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              module Command
         | 
| 13 | 
            +
                class Base
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  attr_reader :table_name
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def initialize(table_name, *args)
         | 
| 18 | 
            +
                    @table_name = table_name
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            module FlareUp
         | 
| 2 | 
            +
              module Command
         | 
| 3 | 
            +
                class Copy < Command::Base
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  attr_reader :data_source
         | 
| 6 | 
            +
                  attr_reader :aws_access_key_id
         | 
| 7 | 
            +
                  attr_reader :aws_secret_access_key
         | 
| 8 | 
            +
                  attr_accessor :options
         | 
| 9 | 
            +
                  attr_reader :columns
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def initialize(table_name, data_source, aws_access_key_id, aws_secret_access_key)
         | 
| 12 | 
            +
                    @data_source = data_source
         | 
| 13 | 
            +
                    @aws_access_key_id = aws_access_key_id
         | 
| 14 | 
            +
                    @aws_secret_access_key = aws_secret_access_key
         | 
| 15 | 
            +
                    @options = ''
         | 
| 16 | 
            +
                    @columns = []
         | 
| 17 | 
            +
                    super
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  # http://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html
         | 
| 21 | 
            +
                  def get_command
         | 
| 22 | 
            +
                    "COPY #{@table_name} #{get_columns} FROM '#{@data_source}' CREDENTIALS '#{get_credentials}' #{@options}"
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def columns=(columns)
         | 
| 26 | 
            +
                    raise ArgumentError, 'Columns must be an array' unless columns.is_a?(Array)
         | 
| 27 | 
            +
                    @columns = columns
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def execute(connection)
         | 
| 31 | 
            +
                    begin
         | 
| 32 | 
            +
                      connection.execute(get_command)
         | 
| 33 | 
            +
                      []
         | 
| 34 | 
            +
                    rescue PG::InternalError => e
         | 
| 35 | 
            +
                      case e.message
         | 
| 36 | 
            +
                        when /Check 'stl_load_errors' system table for details/
         | 
| 37 | 
            +
                          return STLLoadErrorFetcher.fetch_errors(connection)
         | 
| 38 | 
            +
                        when /The specified S3 prefix '.+' does not exist/
         | 
| 39 | 
            +
                          raise DataSourceError, "A data source with prefix '#{@data_source}' does not exist."
         | 
| 40 | 
            +
                        when /The bucket you are attempting to access must be addressed using the specified endpoint/
         | 
| 41 | 
            +
                          raise OtherZoneBucketError, "Your Redshift instance appears to be in a different zone than your S3 bucket.  Specify the \"REGION 'bucket-region'\" option."
         | 
| 42 | 
            +
                        when /PG::SyntaxError/
         | 
| 43 | 
            +
                          matches = /syntax error (.+) \(PG::SyntaxError\)/.match(e.message)
         | 
| 44 | 
            +
                          raise SyntaxError, "Syntax error in the COPY command: [#{matches[1]}]."
         | 
| 45 | 
            +
                        else
         | 
| 46 | 
            +
                          raise e
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  private
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def get_columns
         | 
| 54 | 
            +
                    return '' if columns.empty?
         | 
| 55 | 
            +
                    "(#{@columns.join(', ').strip})"
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def get_credentials
         | 
| 59 | 
            +
                    "aws_access_key_id=#{@aws_access_key_id};aws_secret_access_key=#{@aws_secret_access_key}"
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            module FlareUp
         | 
| 2 | 
            +
              module Command
         | 
| 3 | 
            +
                class CreateTable < Command::Base
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  attr_reader :columns
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def initialize(*args)
         | 
| 8 | 
            +
                    @columns = []
         | 
| 9 | 
            +
                    super
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # http://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html
         | 
| 14 | 
            +
                  def get_command
         | 
| 15 | 
            +
                    "CREATE TABLE #{@table_name} #{get_columns} #{@options}"
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  # a little different than copy... the columns argument will have parentheses
         | 
| 19 | 
            +
                  # (since it specifies a schema, where the datatypes may require parentheses)
         | 
| 20 | 
            +
                  # and will need to be enclosed in quotes, and therefore the argument arrives
         | 
| 21 | 
            +
                  # here as a single membered array. This method corrects this difference between
         | 
| 22 | 
            +
                  # copy and create table by splitting on spaces, so that the columns are stored
         | 
| 23 | 
            +
                  # in the same fashion between the two classes, though they arrive in different
         | 
| 24 | 
            +
                  # forms to this method.
         | 
| 25 | 
            +
                  def columns=(columns)
         | 
| 26 | 
            +
                    raise ArgumentError, 'Columns must be a string' unless columns.is_a?(String)
         | 
| 27 | 
            +
                    columns_separated = columns.split(' ')
         | 
| 28 | 
            +
                    raise ArgumentError, 'Columns must have a data type for each name' unless columns_separated.length % 2 == 0
         | 
| 29 | 
            +
                    @columns = columns_separated
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def execute(connection)
         | 
| 33 | 
            +
                    begin
         | 
| 34 | 
            +
                      connection.execute(get_command)
         | 
| 35 | 
            +
                      []
         | 
| 36 | 
            +
                    rescue PG::InternalError => e
         | 
| 37 | 
            +
                      case e.message
         | 
| 38 | 
            +
                        when /Check 'stl_load_errors' system table for details/
         | 
| 39 | 
            +
                          return STLLoadErrorFetcher.fetch_errors(connection)
         | 
| 40 | 
            +
                        when /PG::SyntaxError/
         | 
| 41 | 
            +
                          matches = /syntax error (.+) \(PG::SyntaxError\)/.match(e.message)
         | 
| 42 | 
            +
                          raise SyntaxError, "Syntax error in the CREATE TABLE command: [#{matches[1]}]."
         | 
| 43 | 
            +
                        else
         | 
| 44 | 
            +
                          raise e
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  private
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def get_columns
         | 
| 52 | 
            +
                    return '' if columns.empty?
         | 
| 53 | 
            +
                    name_type_pairs = @columns.each_slice(2).map { |name, type| "#{name} #{type}" }
         | 
| 54 | 
            +
                    "(#{name_type_pairs.join(', ').strip})"
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            module FlareUp
         | 
| 2 | 
            +
              module Command
         | 
| 3 | 
            +
                class DropTable < Command::Base
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # http://docs.aws.amazon.com/redshift/latest/dg/r_DROP_TABLE.html
         | 
| 6 | 
            +
                  def get_command
         | 
| 7 | 
            +
                    "DROP TABLE #{@table_name} #{@options}"
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def execute(connection)
         | 
| 11 | 
            +
                    begin
         | 
| 12 | 
            +
                      connection.execute(get_command)
         | 
| 13 | 
            +
                      []
         | 
| 14 | 
            +
                    rescue PG::InternalError => e
         | 
| 15 | 
            +
                      case e.message
         | 
| 16 | 
            +
                        when /Check 'stl_load_errors' system table for details/
         | 
| 17 | 
            +
                          return STLLoadErrorFetcher.fetch_errors(connection)
         | 
| 18 | 
            +
                        when /PG::SyntaxError/
         | 
| 19 | 
            +
                          matches = /syntax error (.+) \(PG::SyntaxError\)/.match(e.message)
         | 
| 20 | 
            +
                          raise SyntaxError, "Syntax error in the DROP TABLE command: [#{matches[1]}]."
         | 
| 21 | 
            +
                        else
         | 
| 22 | 
            +
                          raise e
         | 
| 23 | 
            +
                      end
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
    
        data/lib/flare_up/version.rb
    CHANGED
    
    
| @@ -7,7 +7,7 @@ data_source = 's3://slif-redshift/hearthstone_cards_short_list.csv' | |
| 7 7 |  | 
| 8 8 | 
             
            conn = FlareUp::Connection.new(host_name, db_name, ENV['REDSHIFT_USERNAME'], ENV['REDSHIFT_PASSWORD'])
         | 
| 9 9 |  | 
| 10 | 
            -
            copy = FlareUp:: | 
| 10 | 
            +
            copy = FlareUp::Command::Copy.new(table_name, data_source, ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
         | 
| 11 11 | 
             
            copy.columns = %w(name cost attack health description)
         | 
| 12 12 | 
             
            copy.options = "REGION 'us-east-1' CSV"
         | 
| 13 13 |  | 
| @@ -2,7 +2,8 @@ describe FlareUp::Boot do | |
| 2 2 |  | 
| 3 3 | 
             
              describe '.boot' do
         | 
| 4 4 | 
             
                let(:connection) { instance_double('FlareUp::Connection') }
         | 
| 5 | 
            -
                let(:copy_command) { instance_double('FlareUp:: | 
| 5 | 
            +
                let(:copy_command) { instance_double('FlareUp::Command::Copy') }
         | 
| 6 | 
            +
                let(:klass) { FlareUp::Command::Copy }
         | 
| 6 7 |  | 
| 7 8 | 
             
                before do
         | 
| 8 9 | 
             
                  allow(copy_command).to receive(:get_command)
         | 
| @@ -11,7 +12,7 @@ describe FlareUp::Boot do | |
| 11 12 | 
             
                context 'when there is an error connecting' do
         | 
| 12 13 |  | 
| 13 14 | 
             
                  before do
         | 
| 14 | 
            -
                    expect(FlareUp::Boot).to receive(: | 
| 15 | 
            +
                    expect(FlareUp::Boot).to receive(:create_command).and_return(copy_command)
         | 
| 15 16 | 
             
                    expect(copy_command).to receive(:execute).and_raise(copy_command_error)
         | 
| 16 17 | 
             
                  end
         | 
| 17 18 |  | 
| @@ -19,7 +20,7 @@ describe FlareUp::Boot do | |
| 19 20 | 
             
                    let(:copy_command_error) { FlareUp::DataSourceError }
         | 
| 20 21 | 
             
                    it 'should handle the error' do
         | 
| 21 22 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 22 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 23 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 23 24 | 
             
                    end
         | 
| 24 25 | 
             
                  end
         | 
| 25 26 |  | 
| @@ -27,7 +28,7 @@ describe FlareUp::Boot do | |
| 27 28 | 
             
                    let(:copy_command_error) { FlareUp::OtherZoneBucketError }
         | 
| 28 29 | 
             
                    it 'should handle the error' do
         | 
| 29 30 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 30 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 31 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 31 32 | 
             
                    end
         | 
| 32 33 | 
             
                  end
         | 
| 33 34 |  | 
| @@ -35,7 +36,7 @@ describe FlareUp::Boot do | |
| 35 36 | 
             
                    let(:copy_command_error) { FlareUp::SyntaxError }
         | 
| 36 37 | 
             
                    it 'should handle the error' do
         | 
| 37 38 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 38 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 39 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 39 40 | 
             
                    end
         | 
| 40 41 | 
             
                  end
         | 
| 41 42 |  | 
| @@ -52,7 +53,7 @@ describe FlareUp::Boot do | |
| 52 53 | 
             
                    let(:connection_error) { FlareUp::HostUnknownOrInaccessibleError }
         | 
| 53 54 | 
             
                    it 'should handle the error' do
         | 
| 54 55 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 55 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 56 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 56 57 | 
             
                    end
         | 
| 57 58 | 
             
                  end
         | 
| 58 59 |  | 
| @@ -60,7 +61,7 @@ describe FlareUp::Boot do | |
| 60 61 | 
             
                    let(:connection_error) { FlareUp::TimeoutError }
         | 
| 61 62 | 
             
                    it 'should handle the error' do
         | 
| 62 63 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 63 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 64 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 64 65 | 
             
                    end
         | 
| 65 66 | 
             
                  end
         | 
| 66 67 |  | 
| @@ -68,7 +69,7 @@ describe FlareUp::Boot do | |
| 68 69 | 
             
                    let(:connection_error) { FlareUp::NoDatabaseError }
         | 
| 69 70 | 
             
                    it 'should handle the error' do
         | 
| 70 71 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 71 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 72 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 72 73 | 
             
                    end
         | 
| 73 74 | 
             
                  end
         | 
| 74 75 |  | 
| @@ -76,7 +77,7 @@ describe FlareUp::Boot do | |
| 76 77 | 
             
                    let(:connection_error) { FlareUp::AuthenticationError }
         | 
| 77 78 | 
             
                    it 'should handle the error' do
         | 
| 78 79 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 79 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 80 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 80 81 | 
             
                    end
         | 
| 81 82 | 
             
                  end
         | 
| 82 83 |  | 
| @@ -84,7 +85,7 @@ describe FlareUp::Boot do | |
| 84 85 | 
             
                    let(:connection_error) { FlareUp::UnknownError }
         | 
| 85 86 | 
             
                    it 'should handle the error' do
         | 
| 86 87 | 
             
                      expect(FlareUp::CLI).to receive(:bailout).with(1)
         | 
| 87 | 
            -
                      expect { FlareUp::Boot.boot }.not_to raise_error
         | 
| 88 | 
            +
                      expect { FlareUp::Boot.boot klass }.not_to raise_error
         | 
| 88 89 | 
             
                    end
         | 
| 89 90 | 
             
                  end
         | 
| 90 91 |  | 
| @@ -113,7 +114,7 @@ describe FlareUp::Boot do | |
| 113 114 | 
             
                end
         | 
| 114 115 | 
             
              end
         | 
| 115 116 |  | 
| 116 | 
            -
              describe '. | 
| 117 | 
            +
              describe '.create_command' do
         | 
| 117 118 |  | 
| 118 119 | 
             
                before do
         | 
| 119 120 | 
             
                  FlareUp::OptionStore.store_options(
         | 
| @@ -127,7 +128,7 @@ describe FlareUp::Boot do | |
| 127 128 | 
             
                end
         | 
| 128 129 |  | 
| 129 130 | 
             
                it 'should create a proper copy command' do
         | 
| 130 | 
            -
                  command = FlareUp::Boot.send(: | 
| 131 | 
            +
                  command = FlareUp::Boot.send(:create_command, FlareUp::Command::Copy)
         | 
| 131 132 | 
             
                  expect(command.table_name).to eq('TEST_TABLE')
         | 
| 132 133 | 
             
                  expect(command.data_source).to eq('TEST_DATA_SOURCE')
         | 
| 133 134 | 
             
                  expect(command.aws_access_key_id).to eq('TEST_ACCESS_KEY')
         | 
| @@ -139,7 +140,7 @@ describe FlareUp::Boot do | |
| 139 140 | 
             
                    FlareUp::OptionStore.store_option(:column_list, ['c1'])
         | 
| 140 141 | 
             
                  end
         | 
| 141 142 | 
             
                  it 'should create a proper copy command' do
         | 
| 142 | 
            -
                    command = FlareUp::Boot.send(: | 
| 143 | 
            +
                    command = FlareUp::Boot.send(:create_command, FlareUp::Command::Copy)
         | 
| 143 144 | 
             
                    expect(command.columns).to eq(['c1'])
         | 
| 144 145 | 
             
                  end
         | 
| 145 146 | 
             
                end
         | 
| @@ -149,7 +150,7 @@ describe FlareUp::Boot do | |
| 149 150 | 
             
                    FlareUp::OptionStore.store_option(:copy_options, '_')
         | 
| 150 151 | 
             
                  end
         | 
| 151 152 | 
             
                  it 'should create a proper copy command' do
         | 
| 152 | 
            -
                    command = FlareUp::Boot.send(: | 
| 153 | 
            +
                    command = FlareUp::Boot.send(:create_command, FlareUp::Command::Copy)
         | 
| 153 154 | 
             
                    expect(command.options).to eq('_')
         | 
| 154 155 | 
             
                  end
         | 
| 155 156 | 
             
                end
         | 
| @@ -1,7 +1,7 @@ | |
| 1 | 
            -
            describe FlareUp:: | 
| 1 | 
            +
            describe FlareUp::Command::Copy do
         | 
| 2 2 |  | 
| 3 3 | 
             
              subject do
         | 
| 4 | 
            -
                FlareUp:: | 
| 4 | 
            +
                FlareUp::Command::Copy.new('TEST_TABLE_NAME', 'TEST_DATA_SOURCE', 'TEST_ACCESS_KEY', 'TEST_SECRET_KEY')
         | 
| 5 5 | 
             
              end
         | 
| 6 6 |  | 
| 7 7 | 
             
              its(:table_name) { should == 'TEST_TABLE_NAME' }
         | 
| @@ -116,7 +116,6 @@ describe FlareUp::CopyCommand do | |
| 116 116 | 
             
                        expect { subject.execute(conn) }.to raise_error(FlareUp::SyntaxError, 'Syntax error in the COPY command: [at or near "lmlkmlk3"].')
         | 
| 117 117 | 
             
                      end
         | 
| 118 118 | 
             
                    end
         | 
| 119 | 
            -
             | 
| 120 119 | 
             
                  end
         | 
| 121 120 |  | 
| 122 121 | 
             
                  context 'when there was another type of error' do
         | 
| @@ -126,9 +125,6 @@ describe FlareUp::CopyCommand do | |
| 126 125 | 
             
                      expect { subject.execute(conn) }.to raise_error(PG::ConnectionBad, '_')
         | 
| 127 126 | 
             
                    end
         | 
| 128 127 | 
             
                  end
         | 
| 129 | 
            -
             | 
| 130 128 | 
             
                end
         | 
| 131 | 
            -
             | 
| 132 129 | 
             
              end
         | 
| 133 | 
            -
             | 
| 134 | 
            -
            end
         | 
| 130 | 
            +
            end
         | 
| @@ -0,0 +1,117 @@ | |
| 1 | 
            +
            describe FlareUp::Command::CreateTable do
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              subject do
         | 
| 4 | 
            +
                FlareUp::Command::CreateTable.new('TEST_TABLE_NAME', nil, nil, nil)
         | 
| 5 | 
            +
              end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              its(:table_name) { should == 'TEST_TABLE_NAME' }
         | 
| 8 | 
            +
              its(:columns) { should == [] }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              describe '#get_command' do
         | 
| 11 | 
            +
                context 'when no optional fields are provided' do
         | 
| 12 | 
            +
                  it 'should return a basic CREATE TABLE command' do
         | 
| 13 | 
            +
                    expect(subject.get_command).to eq("CREATE TABLE TEST_TABLE_NAME  ")
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                context 'when column names are provided' do
         | 
| 18 | 
            +
                  before do
         | 
| 19 | 
            +
                    subject.columns = 'column_name1 char(24) column_name2 varchar(2000)'
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                  it 'should include the column names in the command' do
         | 
| 22 | 
            +
                    expect(subject.get_command).to start_with('CREATE TABLE TEST_TABLE_NAME (column_name1 char(24), column_name2 varchar(2000))')
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              describe '#columns=' do
         | 
| 28 | 
            +
                context 'when a string' do
         | 
| 29 | 
            +
                  it 'should assign the property' do
         | 
| 30 | 
            +
                    subject.columns = 'column_name1 char(24) column_name2 varchar(2000)'
         | 
| 31 | 
            +
                    expect(subject.columns).to eq(['column_name1', 'char(24)', 'column_name2', 'varchar(2000)'])
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                  it 'should assign an empty array for an empty string' do
         | 
| 34 | 
            +
                    subject.columns = ''
         | 
| 35 | 
            +
                    expect(subject.columns).to eq([])
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                context 'when not a string' do
         | 
| 40 | 
            +
                  it 'should not assign the property and be an error' do
         | 
| 41 | 
            +
                    subject.columns = 'column_name1 char(24)'
         | 
| 42 | 
            +
                    expect {
         | 
| 43 | 
            +
                      subject.columns = ['column_name_in_arr char(24)']
         | 
| 44 | 
            +
                    }.to raise_error(ArgumentError)
         | 
| 45 | 
            +
                    expect(subject.columns).to eq(['column_name1', 'char(24)'])
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                context 'when not a string of name, type pairs' do
         | 
| 50 | 
            +
                  it 'should not assign the property and be an error' do
         | 
| 51 | 
            +
                    subject.columns = 'column_name1 char(24)'
         | 
| 52 | 
            +
                    expect {
         | 
| 53 | 
            +
                      subject.columns = 'column_name1'
         | 
| 54 | 
            +
                    }.to raise_error(ArgumentError)
         | 
| 55 | 
            +
                    expect(subject.columns).to eq(['column_name1', 'char(24)'])
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              describe '#execute' do
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                let(:conn) { instance_double('FlareUp::Connection') }
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                context 'when successful' do
         | 
| 65 | 
            +
                  before do
         | 
| 66 | 
            +
                    expect(conn).to receive(:execute)
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                  it 'should do something' do
         | 
| 69 | 
            +
                    expect(subject.execute(conn)).to eq([])
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                context 'when unsuccessful' do
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  before do
         | 
| 76 | 
            +
                    expect(conn).to receive(:execute).and_raise(exception, message)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  context 'when there was an internal error' do
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    let(:exception) { PG::InternalError }
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    context 'when there was an error loading' do
         | 
| 84 | 
            +
                      let(:message) { "Check 'stl_load_errors' system table for details" }
         | 
| 85 | 
            +
                      before do
         | 
| 86 | 
            +
                        allow(FlareUp::STLLoadErrorFetcher).to receive(:fetch_errors).and_return('FOO')
         | 
| 87 | 
            +
                      end
         | 
| 88 | 
            +
                      it 'should respond with a list of errors' do
         | 
| 89 | 
            +
                        expect(subject.execute(conn)).to eq('FOO')
         | 
| 90 | 
            +
                      end
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    context 'when there was another kind of internal error' do
         | 
| 94 | 
            +
                      let(:message) { '_' }
         | 
| 95 | 
            +
                      it 'should respond with a list of errors' do
         | 
| 96 | 
            +
                        expect { subject.execute(conn) }.to raise_error(PG::InternalError, '_')
         | 
| 97 | 
            +
                      end
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    context 'when there is a syntax error in the command' do
         | 
| 101 | 
            +
                      let(:message) { 'ERROR:  syntax error at or near "lmlkmlk3" (PG::SyntaxError)' }
         | 
| 102 | 
            +
                      it 'should be error' do
         | 
| 103 | 
            +
                        expect { subject.execute(conn) }.to raise_error(FlareUp::SyntaxError, 'Syntax error in the CREATE TABLE command: [at or near "lmlkmlk3"].')
         | 
| 104 | 
            +
                      end
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  context 'when there was another type of error' do
         | 
| 109 | 
            +
                    let(:exception) { PG::ConnectionBad }
         | 
| 110 | 
            +
                    let(:message) { '_' }
         | 
| 111 | 
            +
                    it 'should do something' do
         | 
| 112 | 
            +
                      expect { subject.execute(conn) }.to raise_error(PG::ConnectionBad, '_')
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            describe FlareUp::Command::DropTable do
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              subject do
         | 
| 4 | 
            +
                FlareUp::Command::DropTable.new('TEST_TABLE_NAME', nil, nil, nil)
         | 
| 5 | 
            +
              end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              its(:table_name) { should == 'TEST_TABLE_NAME' }
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              describe '#get_command' do
         | 
| 10 | 
            +
                context 'when no optional fields are provided' do
         | 
| 11 | 
            +
                  it 'should return a basic DROP TABLE command' do
         | 
| 12 | 
            +
                    expect(subject.get_command).to eq("DROP TABLE TEST_TABLE_NAME ")
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              describe '#execute' do
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                let(:conn) { instance_double('FlareUp::Connection') }
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                context 'when successful' do
         | 
| 22 | 
            +
                  before do
         | 
| 23 | 
            +
                    expect(conn).to receive(:execute)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                  it 'should do something' do
         | 
| 26 | 
            +
                    expect(subject.execute(conn)).to eq([])
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                context 'when unsuccessful' do
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  before do
         | 
| 33 | 
            +
                    expect(conn).to receive(:execute).and_raise(exception, message)
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  context 'when there was an internal error' do
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    let(:exception) { PG::InternalError }
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    context 'when there was an error loading' do
         | 
| 41 | 
            +
                      let(:message) { "Check 'stl_load_errors' system table for details" }
         | 
| 42 | 
            +
                      before do
         | 
| 43 | 
            +
                        allow(FlareUp::STLLoadErrorFetcher).to receive(:fetch_errors).and_return('FOO')
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                      it 'should respond with a list of errors' do
         | 
| 46 | 
            +
                        expect(subject.execute(conn)).to eq('FOO')
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    context 'when there was another kind of internal error' do
         | 
| 51 | 
            +
                      let(:message) { '_' }
         | 
| 52 | 
            +
                      it 'should respond with a list of errors' do
         | 
| 53 | 
            +
                        expect { subject.execute(conn) }.to raise_error(PG::InternalError, '_')
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    context 'when there is a syntax error in the command' do
         | 
| 58 | 
            +
                      let(:message) { 'ERROR:  syntax error at or near "lmlkmlk3" (PG::SyntaxError)' }
         | 
| 59 | 
            +
                      it 'should be error' do
         | 
| 60 | 
            +
                        expect { subject.execute(conn) }.to raise_error(FlareUp::SyntaxError, 'Syntax error in the DROP TABLE command: [at or near "lmlkmlk3"].')
         | 
| 61 | 
            +
                      end
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  context 'when there was another type of error' do
         | 
| 66 | 
            +
                    let(:exception) { PG::ConnectionBad }
         | 
| 67 | 
            +
                    let(:message) { '_' }
         | 
| 68 | 
            +
                    it 'should do something' do
         | 
| 69 | 
            +
                      expect { subject.execute(conn) }.to raise_error(PG::ConnectionBad, '_')
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: flare-up
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: '0. | 
| 4 | 
            +
              version: '0.9'
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Robert Slifka
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2014-11- | 
| 11 | 
            +
            date: 2014-11-17 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: pg
         | 
| @@ -81,7 +81,8 @@ dependencies: | |
| 81 81 | 
             
                  - !ruby/object:Gem::Version
         | 
| 82 82 | 
             
                    version: '1.0'
         | 
| 83 83 | 
             
            description: Flare-up makes Redshift COPY scriptable by providing CLI access to the
         | 
| 84 | 
            -
              Redshift COPY command, with handy access to pretty printed errors as well.
         | 
| 84 | 
            +
              Redshift COPY command, with handy access to pretty printed errors as well. It also
         | 
| 85 | 
            +
              includes the CREATE TABLE and DROP TABLE commands.
         | 
| 85 86 | 
             
            email: 
         | 
| 86 87 | 
             
            executables:
         | 
| 87 88 | 
             
            - flare-up
         | 
| @@ -102,8 +103,11 @@ files: | |
| 102 103 | 
             
            - lib/flare_up.rb
         | 
| 103 104 | 
             
            - lib/flare_up/boot.rb
         | 
| 104 105 | 
             
            - lib/flare_up/cli.rb
         | 
| 106 | 
            +
            - lib/flare_up/command/base.rb
         | 
| 107 | 
            +
            - lib/flare_up/command/copy.rb
         | 
| 108 | 
            +
            - lib/flare_up/command/create_table.rb
         | 
| 109 | 
            +
            - lib/flare_up/command/drop_table.rb
         | 
| 105 110 | 
             
            - lib/flare_up/connection.rb
         | 
| 106 | 
            -
            - lib/flare_up/copy_command.rb
         | 
| 107 111 | 
             
            - lib/flare_up/emitter.rb
         | 
| 108 112 | 
             
            - lib/flare_up/env_wrap.rb
         | 
| 109 113 | 
             
            - lib/flare_up/option_store.rb
         | 
| @@ -117,8 +121,10 @@ files: | |
| 117 121 | 
             
            - resources/test_schema.sql
         | 
| 118 122 | 
             
            - spec/lib/flare_up/boot_spec.rb
         | 
| 119 123 | 
             
            - spec/lib/flare_up/cli_spec.rb
         | 
| 124 | 
            +
            - spec/lib/flare_up/command/copy_spec.rb
         | 
| 125 | 
            +
            - spec/lib/flare_up/command/create_table_spec.rb
         | 
| 126 | 
            +
            - spec/lib/flare_up/command/drop_table_spec.rb
         | 
| 120 127 | 
             
            - spec/lib/flare_up/connection_spec.rb
         | 
| 121 | 
            -
            - spec/lib/flare_up/copy_command_spec.rb
         | 
| 122 128 | 
             
            - spec/lib/flare_up/emitter_spec.rb
         | 
| 123 129 | 
             
            - spec/lib/flare_up/option_store_spec.rb
         | 
| 124 130 | 
             
            - spec/lib/flare_up/stl_load_error_fetcher_spec.rb
         | 
| @@ -146,12 +152,15 @@ rubyforge_project: | |
| 146 152 | 
             
            rubygems_version: 2.2.2
         | 
| 147 153 | 
             
            signing_key: 
         | 
| 148 154 | 
             
            specification_version: 4
         | 
| 149 | 
            -
            summary: Command-line access to bulk data loading via Redshift's COPY | 
| 155 | 
            +
            summary: Command-line access to bulk data loading via Redshift's CREATE TABLE, COPY,
         | 
| 156 | 
            +
              and DROP TABLE commands.
         | 
| 150 157 | 
             
            test_files:
         | 
| 151 158 | 
             
            - spec/lib/flare_up/boot_spec.rb
         | 
| 152 159 | 
             
            - spec/lib/flare_up/cli_spec.rb
         | 
| 160 | 
            +
            - spec/lib/flare_up/command/copy_spec.rb
         | 
| 161 | 
            +
            - spec/lib/flare_up/command/create_table_spec.rb
         | 
| 162 | 
            +
            - spec/lib/flare_up/command/drop_table_spec.rb
         | 
| 153 163 | 
             
            - spec/lib/flare_up/connection_spec.rb
         | 
| 154 | 
            -
            - spec/lib/flare_up/copy_command_spec.rb
         | 
| 155 164 | 
             
            - spec/lib/flare_up/emitter_spec.rb
         | 
| 156 165 | 
             
            - spec/lib/flare_up/option_store_spec.rb
         | 
| 157 166 | 
             
            - spec/lib/flare_up/stl_load_error_fetcher_spec.rb
         | 
| @@ -1,73 +0,0 @@ | |
| 1 | 
            -
            module FlareUp
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              class CopyCommandError < StandardError
         | 
| 4 | 
            -
              end
         | 
| 5 | 
            -
              class DataSourceError < CopyCommandError
         | 
| 6 | 
            -
              end
         | 
| 7 | 
            -
              class OtherZoneBucketError < CopyCommandError
         | 
| 8 | 
            -
              end
         | 
| 9 | 
            -
              class SyntaxError < CopyCommandError
         | 
| 10 | 
            -
              end
         | 
| 11 | 
            -
             | 
| 12 | 
            -
              class CopyCommand
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                attr_reader :table_name
         | 
| 15 | 
            -
                attr_reader :data_source
         | 
| 16 | 
            -
                attr_reader :aws_access_key_id
         | 
| 17 | 
            -
                attr_reader :aws_secret_access_key
         | 
| 18 | 
            -
                attr_reader :columns
         | 
| 19 | 
            -
                attr_accessor :options
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                def initialize(table_name, data_source, aws_access_key_id, aws_secret_access_key)
         | 
| 22 | 
            -
                  @table_name = table_name
         | 
| 23 | 
            -
                  @data_source = data_source
         | 
| 24 | 
            -
                  @aws_access_key_id = aws_access_key_id
         | 
| 25 | 
            -
                  @aws_secret_access_key = aws_secret_access_key
         | 
| 26 | 
            -
                  @columns = []
         | 
| 27 | 
            -
                  @options = ''
         | 
| 28 | 
            -
                end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                def get_command
         | 
| 31 | 
            -
                  "COPY #{@table_name} #{get_columns} FROM '#{@data_source}' CREDENTIALS '#{get_credentials}' #{@options}"
         | 
| 32 | 
            -
                end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                def columns=(columns)
         | 
| 35 | 
            -
                  raise ArgumentError, 'Columns must be an array' unless columns.is_a?(Array)
         | 
| 36 | 
            -
                  @columns = columns
         | 
| 37 | 
            -
                end
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                def execute(connection)
         | 
| 40 | 
            -
                  begin
         | 
| 41 | 
            -
                    connection.execute(get_command)
         | 
| 42 | 
            -
                    []
         | 
| 43 | 
            -
                  rescue PG::InternalError => e
         | 
| 44 | 
            -
                    case e.message
         | 
| 45 | 
            -
                      when /Check 'stl_load_errors' system table for details/
         | 
| 46 | 
            -
                        return STLLoadErrorFetcher.fetch_errors(connection)
         | 
| 47 | 
            -
                      when /The specified S3 prefix '.+' does not exist/
         | 
| 48 | 
            -
                        raise DataSourceError, "A data source with prefix '#{@data_source}' does not exist."
         | 
| 49 | 
            -
                      when /The bucket you are attempting to access must be addressed using the specified endpoint/
         | 
| 50 | 
            -
                        raise OtherZoneBucketError, "Your Redshift instance appears to be in a different zone than your S3 bucket.  Specify the \"REGION 'bucket-region'\" option."
         | 
| 51 | 
            -
                      when /PG::SyntaxError/
         | 
| 52 | 
            -
                        matches = /syntax error (.+) \(PG::SyntaxError\)/.match(e.message)
         | 
| 53 | 
            -
                        raise SyntaxError, "Syntax error in the COPY command: [#{matches[1]}]."
         | 
| 54 | 
            -
                      else
         | 
| 55 | 
            -
                        raise e
         | 
| 56 | 
            -
                    end
         | 
| 57 | 
            -
                  end
         | 
| 58 | 
            -
                end
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                private
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                def get_columns
         | 
| 63 | 
            -
                  return '' if columns.empty?
         | 
| 64 | 
            -
                  "(#{@columns.join(', ').strip})"
         | 
| 65 | 
            -
                end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                def get_credentials
         | 
| 68 | 
            -
                  "aws_access_key_id=#{@aws_access_key_id};aws_secret_access_key=#{@aws_secret_access_key}"
         | 
| 69 | 
            -
                end
         | 
| 70 | 
            -
             | 
| 71 | 
            -
              end
         | 
| 72 | 
            -
             | 
| 73 | 
            -
            end
         |