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 +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
|