flare-up 0.8 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NzFmZDVjOWIyZmE4MDcxODhhM2E1YjhmNmE3YmNiODA2NWZhMjhkMg==
4
+ MWI2ZDkyYjVjOTBlMWMyYjI2NzM3NjNhNThkYzFlM2Q5OWYxNDQyNw==
5
5
  data.tar.gz: !binary |-
6
- Y2ZmYTExMTRhMWUxYTdlNDQ4NjA5ZmRmODJiMjQxMDdmNjQ2ZGQyNg==
6
+ OTU0NTBiOGExMTA4MjhjOWViNzllMjUwMWU3YjU5NTcyODVlMmFhZA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MGY5YjZjNjI1ZWExZTRhODMyNWU2MzgzMGUyZTQ4NjJjNTk5MjYxNTliNzcw
10
- N2M0ZDY0NWZlMmZjZWZiYzc2NTE3NjA3OTRjNTAxNDQ1OGYyMzc3MDFlOWMw
11
- NzhlMjlmNDBkMjQ3ZGQ2ZjZhMGFhOTE1NmYyMDZjMjg0NDI5NjY=
9
+ MTM0MDVlMzAxOGUxOGQzNTRkNTQwMDBmNTQ4ZmU2MjY0OTRmZTI5YjY3OTZi
10
+ ZTBiYTUxNjk5YjZiYTI5ZDRkYjI5NDljOWY0YmI2NjBkYmZiMjFkZDgyZWVl
11
+ YmNmMjZjOWM0OWZmNmE0ZjZjN2E4OTlhZTNkOGFjY2E3MDY5MWY=
12
12
  data.tar.gz: !binary |-
13
- MDFjZTI1MDU0OGE2OTcyYmMwOGM1OWIyYWJiYWJmNjFmMGVlYWM4ZWYxMjM2
14
- YzIxMmEzMmZmYWY0OTg5YTJlMDRjYjJiMWM3NDExMTUxMGM1OGMxNGE2ZmRi
15
- ZDhkNmM1ZmJjOTYyYmIxZDE3NmNkM2ZiMDFmOWMzZDU5MjJkNWU=
13
+ OWJkODg5ZmMyOGRjNDljZGIwNmE3MDdlMWMyMjUzZTBhY2Q5YjQ4MDNiN2Iw
14
+ MTUxZWVjZTE4YmIyZDY4ZjU1ZGQ2ZjEzZTgzMGFlYmU4OTY3NmIyZGVhNWNj
15
+ YmNkMDlhZmNhMzAzMzg1NmZjZjM4MWIyOGI1OTk1MDY1MGFmMDE=
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg/
2
2
  .DS_Store
3
3
  .env
4
+ makefile
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flare-up (0.8)
4
+ flare-up (0.9)
5
5
  pg (~> 0.17)
6
6
  thor (~> 0.19)
7
7
 
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) command for scriptability, allowing you to issue the command 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.
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 one than one job flow control tool is two job flow control tools :)
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 this example assumes you have credentials set as environment variables.
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
  ```
@@ -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 command.}
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')
@@ -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/copy_command'
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'
@@ -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
- copy = create_copy_command
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: #{copy.get_command}")
23
- handle_load_errors(copy.execute(conn))
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 CopyCommandError => e
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.create_copy_command
45
- copy = FlareUp::CopyCommand.new(
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
- copy.columns = OptionStore.get(:column_list) if OptionStore.get(:column_list)
52
- copy.options = OptionStore.get(:copy_options) if OptionStore.get(:copy_options)
53
- copy
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 :create_copy_command
55
+ private_class_method :create_command
56
56
 
57
57
  # TODO: Backfill tests
58
58
  def self.handle_load_errors(stl_load_errors)
@@ -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
- option :copy_options, :type => :string, :desc => "Appended to the end of the COPY command; enclose \"IN QUOTES\""
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
- boot_options = {
23
- :data_source => data_source,
24
- :redshift_endpoint => endpoint,
25
- :database => database_name,
26
- :table => table_name
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
- OptionStore.store_options(boot_options)
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
- Boot.boot
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
@@ -1,3 +1,3 @@
1
1
  module FlareUp
2
- VERSION = '0.8'
2
+ VERSION = '0.9'
3
3
  end
@@ -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::CopyCommand.new(table_name, data_source, ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
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::CopyCommand') }
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(:create_copy_command).and_return(copy_command)
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 '.create_copy_command' do
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(:create_copy_command)
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(:create_copy_command)
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(:create_copy_command)
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::CopyCommand do
1
+ describe FlareUp::Command::Copy do
2
2
 
3
3
  subject do
4
- FlareUp::CopyCommand.new('TEST_TABLE_NAME', 'TEST_DATA_SOURCE', 'TEST_ACCESS_KEY', 'TEST_SECRET_KEY')
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.8'
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-13 00:00:00.000000000 Z
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 command.
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