dynomite 1.0.5
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile +6 -0
- data/README.md +141 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/migrations/long-example.rb +123 -0
- data/docs/migrations/short-example.rb +36 -0
- data/dynomite.gemspec +29 -0
- data/lib/dynomite.rb +23 -0
- data/lib/dynomite/core.rb +25 -0
- data/lib/dynomite/db_config.rb +101 -0
- data/lib/dynomite/erb.rb +53 -0
- data/lib/dynomite/item.rb +291 -0
- data/lib/dynomite/log.rb +15 -0
- data/lib/dynomite/migration.rb +27 -0
- data/lib/dynomite/migration/common.rb +86 -0
- data/lib/dynomite/migration/dsl.rb +172 -0
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +72 -0
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +4 -0
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +8 -0
- data/lib/dynomite/migration/executor.rb +30 -0
- data/lib/dynomite/migration/generator.rb +68 -0
- data/lib/dynomite/migration/templates/create_table.rb +32 -0
- data/lib/dynomite/migration/templates/update_table.rb +26 -0
- data/lib/dynomite/version.rb +3 -0
- metadata +141 -0
data/lib/dynomite/log.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Dynomite
|
2
|
+
class Migration
|
3
|
+
autoload :Dsl, "dynomite/migration/dsl"
|
4
|
+
autoload :Generator, "dynomite/migration/generator"
|
5
|
+
autoload :Executor, "dynomite/migration/executor"
|
6
|
+
|
7
|
+
def up
|
8
|
+
puts "Should defined an up method for your migration: #{self.class.name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_table(table_name, &block)
|
12
|
+
execute_with_dsl_params(table_name, :create_table, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def update_table(table_name, &block)
|
16
|
+
execute_with_dsl_params(table_name, :update_table, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def execute_with_dsl_params(table_name, method_name, &block)
|
21
|
+
dsl = Dsl.new(method_name, table_name, &block)
|
22
|
+
params = dsl.params
|
23
|
+
executor = Executor.new(table_name, method_name, params)
|
24
|
+
executor.run
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Common methods to the *SecondaryIndex classes that handle gsi and lsi methods
|
2
|
+
# as well a the Dsl class that handles create_table and update_table methods.
|
3
|
+
class Dynomite::Migration::Dsl
|
4
|
+
module Common
|
5
|
+
# http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Types/KeySchemaElement.html
|
6
|
+
# partition_key is required
|
7
|
+
def partition_key(identifier)
|
8
|
+
@partition_key_identifier = identifier # for later use. useful for conventional_index_name
|
9
|
+
adjust_schema_and_attributes(identifier, "hash")
|
10
|
+
end
|
11
|
+
|
12
|
+
# sort_key is optional
|
13
|
+
def sort_key(identifier)
|
14
|
+
@sort_key_identifier = identifier # for later use. useful for conventional_index_name
|
15
|
+
adjust_schema_and_attributes(identifier, "range")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Parameters:
|
19
|
+
# identifier: "id:string" or "id"
|
20
|
+
# key_type: "hash" or "range"
|
21
|
+
#
|
22
|
+
# Adjusts the parameters for create_table to add the
|
23
|
+
# partition_key and sort_key
|
24
|
+
def adjust_schema_and_attributes(identifier, key_type)
|
25
|
+
name, attribute_type = identifier.split(':')
|
26
|
+
attribute_type = "string" if attribute_type.nil?
|
27
|
+
|
28
|
+
partition_key = {
|
29
|
+
attribute_name: name,
|
30
|
+
key_type: key_type.upcase
|
31
|
+
}
|
32
|
+
@key_schema << partition_key
|
33
|
+
|
34
|
+
attribute_definition = {
|
35
|
+
attribute_name: name,
|
36
|
+
attribute_type: Dynomite::ATTRIBUTE_TYPES[attribute_type]
|
37
|
+
}
|
38
|
+
@attribute_definitions << attribute_definition
|
39
|
+
end
|
40
|
+
|
41
|
+
# t.provisioned_throughput(5) # both
|
42
|
+
# t.provisioned_throughput(:read, 5)
|
43
|
+
# t.provisioned_throughput(:write, 5)
|
44
|
+
# t.provisioned_throughput(:both, 5)
|
45
|
+
def provisioned_throughput(*params)
|
46
|
+
case params.size
|
47
|
+
when 0 # reader method
|
48
|
+
return @provisioned_throughput # early return
|
49
|
+
when 1
|
50
|
+
# @provisioned_throughput_set_called useful for update_table
|
51
|
+
# only provide a provisioned_throughput settings if explicitly called for update_table
|
52
|
+
@provisioned_throughput_set_called = true
|
53
|
+
arg = params[0]
|
54
|
+
if arg.is_a?(Hash)
|
55
|
+
# Case:
|
56
|
+
# provisioned_throughput(
|
57
|
+
# read_capacity_units: 10,
|
58
|
+
# write_capacity_units: 10
|
59
|
+
# )
|
60
|
+
@provisioned_throughput = arg # set directly
|
61
|
+
return # early return
|
62
|
+
else # assume parameter is an Integer
|
63
|
+
# Case: provisioned_throughput(10)
|
64
|
+
capacity_type = :both
|
65
|
+
capacity_units = arg
|
66
|
+
end
|
67
|
+
when 2
|
68
|
+
@provisioned_throughput_set_called = true
|
69
|
+
# Case: provisioned_throughput(:read, 5)
|
70
|
+
capacity_type, capacity_units = params
|
71
|
+
end
|
72
|
+
|
73
|
+
map = {
|
74
|
+
read: :read_capacity_units,
|
75
|
+
write: :write_capacity_units,
|
76
|
+
}
|
77
|
+
|
78
|
+
if capacity_type == :both
|
79
|
+
@provisioned_throughput[map[:read]] = capacity_units
|
80
|
+
@provisioned_throughput[map[:write]] = capacity_units
|
81
|
+
else
|
82
|
+
@provisioned_throughput[capacity_type] = capacity_units
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
class Dynomite::Migration
|
2
|
+
class Dsl
|
3
|
+
autoload :Common, "dynomite/migration/common"
|
4
|
+
autoload :BaseSecondaryIndex, "dynomite/migration/dsl/base_secondary_index"
|
5
|
+
autoload :LocalSecondaryIndex, "dynomite/migration/dsl/local_secondary_index"
|
6
|
+
autoload :GlobalSecondaryIndex, "dynomite/migration/dsl/global_secondary_index"
|
7
|
+
|
8
|
+
include Dynomite::DbConfig
|
9
|
+
include Common
|
10
|
+
|
11
|
+
attr_accessor :key_schema, :attribute_definitions
|
12
|
+
attr_accessor :table_name
|
13
|
+
def initialize(method_name, table_name, &block)
|
14
|
+
@method_name = method_name
|
15
|
+
@table_name = table_name
|
16
|
+
@block = block
|
17
|
+
|
18
|
+
# Dsl fills in atttributes in as methods are called within the block.
|
19
|
+
# Attributes for both create_table and updated_table:
|
20
|
+
@attribute_definitions = []
|
21
|
+
@provisioned_throughput = {
|
22
|
+
read_capacity_units: 5,
|
23
|
+
write_capacity_units: 5
|
24
|
+
}
|
25
|
+
|
26
|
+
# Attributes for create_table only:
|
27
|
+
@key_schema = []
|
28
|
+
|
29
|
+
# Attributes for update_table only:
|
30
|
+
@gsi_indexes = []
|
31
|
+
@lsi_indexes = []
|
32
|
+
end
|
33
|
+
|
34
|
+
# t.gsi(:create) do |i|
|
35
|
+
# i.partition_key = "category:string"
|
36
|
+
# i.sort_key = "created_at:string" # optional
|
37
|
+
# end
|
38
|
+
def gsi(action, index_name=nil, &block)
|
39
|
+
gsi_index = GlobalSecondaryIndex.new(action, index_name, &block)
|
40
|
+
@gsi_indexes << gsi_index # store @gsi_index for the parent Dsl to use
|
41
|
+
end
|
42
|
+
alias_method :global_secondary_index, :gsi
|
43
|
+
|
44
|
+
# t.lsi(:create) do |i|
|
45
|
+
# i.partition_key = "category:string"
|
46
|
+
# i.sort_key = "created_at:string" # optional
|
47
|
+
# end
|
48
|
+
def lsi(action=:create, index_name=nil, &block)
|
49
|
+
# dont need action create but have it to keep the lsi and gsi method consistent
|
50
|
+
lsi_index = LocalSecondaryIndex.new(index_name, &block)
|
51
|
+
@lsi_indexes << lsi_index # store @lsi_index for the parent Dsl to use
|
52
|
+
end
|
53
|
+
alias_method :local_secondary_index, :gsi
|
54
|
+
|
55
|
+
def evaluate
|
56
|
+
return if @evaluated
|
57
|
+
@block.call(self) if @block
|
58
|
+
@evaluated = true
|
59
|
+
end
|
60
|
+
|
61
|
+
# http://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/dynamo-example-create-table.html
|
62
|
+
# build the params up from dsl in memory and provides params to the
|
63
|
+
# executor
|
64
|
+
def params
|
65
|
+
evaluate # lazy evaluation: wait until as long as possible before evaluating code block
|
66
|
+
|
67
|
+
# Not using send because think its clearer in this case
|
68
|
+
case @method_name
|
69
|
+
when :create_table
|
70
|
+
params_create_table
|
71
|
+
when :update_table
|
72
|
+
params_update_table
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def params_create_table
|
77
|
+
params = {
|
78
|
+
table_name: namespaced_table_name,
|
79
|
+
key_schema: @key_schema,
|
80
|
+
attribute_definitions: @attribute_definitions,
|
81
|
+
provisioned_throughput: @provisioned_throughput
|
82
|
+
}
|
83
|
+
|
84
|
+
params[:local_secondary_index_creates] = local_secondary_index_creates unless @lsi_indexes.empty?
|
85
|
+
params
|
86
|
+
end
|
87
|
+
|
88
|
+
# Goes thorugh all the lsi_indexes that have been built up in memory.
|
89
|
+
# Find the lsi object that creates an index and then grab the
|
90
|
+
# attribute_definitions from it.
|
91
|
+
def lsi_create_attribute_definitions
|
92
|
+
lsi = @lsi_indexes.first # DynamoDB only supports adding one index at a time anyway. The reason @lsi_indexes is an Array is because we're sharing the same class code for LSI and GSI
|
93
|
+
if lsi
|
94
|
+
lsi.evaluate # force early evaluate since we need the params to
|
95
|
+
# add: gsi_attribute_definitions + lsi_attrs
|
96
|
+
lsi_attrs = lsi.attribute_definitions
|
97
|
+
end
|
98
|
+
all_attrs = if lsi_attrs
|
99
|
+
@attribute_definitions + lsi_attrs
|
100
|
+
else
|
101
|
+
@attribute_definitions
|
102
|
+
end
|
103
|
+
all_attrs.uniq
|
104
|
+
end
|
105
|
+
|
106
|
+
# maps each lsi to the hash structure expected by dynamodb update_table
|
107
|
+
# under the global_secondary_index_updates key:
|
108
|
+
#
|
109
|
+
# { create: {...} }
|
110
|
+
# { update: {...} }
|
111
|
+
# { delete: {...} }
|
112
|
+
def lsi_secondary_index_creates
|
113
|
+
@lsi_indexes.map do |lsi|
|
114
|
+
{ lsi.action => lsi.params }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def params_update_table
|
119
|
+
params = {
|
120
|
+
table_name: namespaced_table_name,
|
121
|
+
attribute_definitions: gsi_create_attribute_definitions,
|
122
|
+
# update table take values only some values for the "parent" table
|
123
|
+
# no key_schema, update_table does not handle key_schema for the "parent" table
|
124
|
+
}
|
125
|
+
# only set "parent" table provisioned_throughput if user actually invoked
|
126
|
+
# it in the dsl
|
127
|
+
params[:provisioned_throughput] = @provisioned_throughput if @provisioned_throughput_set_called
|
128
|
+
params[:global_secondary_index_updates] = global_secondary_index_updates
|
129
|
+
params
|
130
|
+
end
|
131
|
+
|
132
|
+
# Goes thorugh all the gsi_indexes that have been built up in memory.
|
133
|
+
# Find the gsi object that creates an index and then grab the
|
134
|
+
# attribute_definitions from it.
|
135
|
+
def gsi_create_attribute_definitions
|
136
|
+
gsi = @gsi_indexes.find { |gsi| gsi.action == :create }
|
137
|
+
if gsi
|
138
|
+
gsi.evaluate # force early evaluate since we need the params to
|
139
|
+
# add: gsi_attribute_definitions + gsi_attrs
|
140
|
+
gsi_attrs = gsi.attribute_definitions
|
141
|
+
end
|
142
|
+
all_attrs = if gsi_attrs
|
143
|
+
gsi_attribute_definitions + gsi_attrs
|
144
|
+
else
|
145
|
+
gsi_attribute_definitions
|
146
|
+
end
|
147
|
+
all_attrs.uniq
|
148
|
+
end
|
149
|
+
|
150
|
+
# maps each gsi to the hash structure expected by dynamodb update_table
|
151
|
+
# under the global_secondary_index_updates key:
|
152
|
+
#
|
153
|
+
# { create: {...} }
|
154
|
+
# { update: {...} }
|
155
|
+
# { delete: {...} }
|
156
|
+
def global_secondary_index_updates
|
157
|
+
@gsi_indexes.map do |gsi|
|
158
|
+
{ gsi.action => gsi.params }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# >> resp = Post.db.describe_table(table_name: "demo-dev-posts")
|
163
|
+
# >> resp.table.attribute_definitions.map(&:to_h)
|
164
|
+
# => [{:attribute_name=>"id", :attribute_type=>"S"}]
|
165
|
+
def gsi_attribute_definitions
|
166
|
+
return @gsi_attribute_definitions if @gsi_attribute_definitions
|
167
|
+
|
168
|
+
resp = db.describe_table(table_name: namespaced_table_name)
|
169
|
+
@gsi_attribute_definitions = resp.table.attribute_definitions.map(&:to_h)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Base class for LocalSecondaryIndex and GlobalSecondaryIndex
|
2
|
+
class Dynomite::Migration::Dsl
|
3
|
+
class BaseSecondaryIndex
|
4
|
+
include Common
|
5
|
+
|
6
|
+
attr_accessor :action, :key_schema, :attribute_definitions
|
7
|
+
attr_accessor :index_name
|
8
|
+
def initialize(action, index_name=nil, &block)
|
9
|
+
@action = action.to_sym
|
10
|
+
# for gsi action can be: :create, :update, :delete
|
11
|
+
# for lsi action is always: :create
|
12
|
+
@index_name = index_name
|
13
|
+
@block = block
|
14
|
+
|
15
|
+
# Dsl fills these atttributes in as methods are called within
|
16
|
+
# the block
|
17
|
+
@key_schema = []
|
18
|
+
@attribute_definitions = []
|
19
|
+
# default provisioned_throughput
|
20
|
+
@provisioned_throughput = {
|
21
|
+
read_capacity_units: 5,
|
22
|
+
write_capacity_units: 5
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def index_name
|
27
|
+
@index_name || conventional_index_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def conventional_index_name
|
31
|
+
# @partition_key_identifier and @sort_key_identifier are set as immediately
|
32
|
+
# when the partition_key and sort_key methods are called in the dsl block.
|
33
|
+
# Usually look like this:
|
34
|
+
#
|
35
|
+
# @partition_key_identifier: post_id:string
|
36
|
+
# @sort_key_identifier: updated_at:string
|
37
|
+
#
|
38
|
+
# We strip the :string portion in case it is provided
|
39
|
+
#
|
40
|
+
partition_key = @partition_key_identifier.split(':').first
|
41
|
+
sort_key = @sort_key_identifier.split(':').first if @sort_key_identifier
|
42
|
+
[partition_key, sort_key, "index"].compact.join('-')
|
43
|
+
end
|
44
|
+
|
45
|
+
def evaluate
|
46
|
+
return if @evaluated
|
47
|
+
@block.call(self) if @block
|
48
|
+
@evaluated = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def params
|
52
|
+
evaluate # lazy evaluation: wait until as long as possible before evaluating code block
|
53
|
+
|
54
|
+
params = { index_name: index_name } # required for all actions
|
55
|
+
|
56
|
+
if @action == :create
|
57
|
+
params[:key_schema] = @key_schema # required for create action
|
58
|
+
# hardcode to ALL for now
|
59
|
+
params[:projection] = { # required
|
60
|
+
projection_type: "ALL", # accepts ALL, KEYS_ONLY, INCLUDE
|
61
|
+
# non_key_attributes: ["NonKeyAttributeName"],
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
if [:create, :update].include?(@action)
|
66
|
+
params[:provisioned_throughput] = @provisioned_throughput
|
67
|
+
end
|
68
|
+
|
69
|
+
params
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Dynomite::Migration
|
2
|
+
class Executor
|
3
|
+
include Dynomite::DbConfig
|
4
|
+
|
5
|
+
# Examples:
|
6
|
+
# Executor.new(:create_table, params) or
|
7
|
+
# Executor.new(:update_table, params)
|
8
|
+
#
|
9
|
+
# The params are generated frmo the dsl.params
|
10
|
+
attr_accessor :table_name
|
11
|
+
def initialize(table_name, method_name, params)
|
12
|
+
@table_name = table_name
|
13
|
+
@method_name = method_name # create_table or update_table
|
14
|
+
@params = params
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
begin
|
19
|
+
# Examples:
|
20
|
+
# result = db.create_table(@params)
|
21
|
+
# result = db.update_table(@params)
|
22
|
+
result = db.send(@method_name, @params)
|
23
|
+
|
24
|
+
puts "DynamoDB Table: #{@table_name} Status: #{result.table_description.table_status}"
|
25
|
+
rescue Aws::DynamoDB::Errors::ServiceError => error
|
26
|
+
puts "Unable to #{@method_name.to_s.gsub('_',' ')}: #{error.message}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "active_support/core_ext/string"
|
2
|
+
|
3
|
+
class Dynomite::Migration
|
4
|
+
# jets dynamodb:generate posts --partition-key id:string
|
5
|
+
class Generator
|
6
|
+
include Dynomite::DbConfig
|
7
|
+
|
8
|
+
attr_reader :migration_name, :table_name
|
9
|
+
def initialize(migration_name, options)
|
10
|
+
@migration_name = migration_name
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
puts "Generating migration for #{@table_name}" unless @options[:quiet]
|
16
|
+
return if @options[:noop]
|
17
|
+
create_migration
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_migration
|
21
|
+
FileUtils.mkdir_p(File.dirname(migration_path))
|
22
|
+
IO.write(migration_path, migration_code)
|
23
|
+
puts "Migration file created: #{migration_path}. \nTo run:"
|
24
|
+
puts " jets dynamodb:migrate #{migration_path}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def migration_code
|
28
|
+
path = File.expand_path("../templates/#{table_action}.rb", __FILE__)
|
29
|
+
result = Dynomite::Erb.result(path,
|
30
|
+
migration_class_name: migration_class_name,
|
31
|
+
table_name: table_name,
|
32
|
+
partition_key: @options[:partition_key],
|
33
|
+
sort_key: @options[:sort_key],
|
34
|
+
provisioned_throughput: @options[:provisioned_throughput] || 5,
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def table_action
|
39
|
+
@options[:table_action] || conventional_table_action
|
40
|
+
end
|
41
|
+
|
42
|
+
def conventional_table_action
|
43
|
+
@migration_name.include?("update") ? "update_table" : "create_table"
|
44
|
+
end
|
45
|
+
|
46
|
+
def table_name
|
47
|
+
@options[:table_name] || conventional_table_name
|
48
|
+
end
|
49
|
+
|
50
|
+
# create_posts => posts
|
51
|
+
# update_posts => posts
|
52
|
+
def conventional_table_name
|
53
|
+
@migration_name.sub(/^(create|update)_/, '')
|
54
|
+
end
|
55
|
+
|
56
|
+
def migration_class_name
|
57
|
+
"#{@migration_name}_migration".classify # doesnt include timestamp
|
58
|
+
end
|
59
|
+
|
60
|
+
def migration_path
|
61
|
+
"#{Dynomite.app_root}dynamodb/migrate/#{timestamp}-#{@migration_name}_migration.rb"
|
62
|
+
end
|
63
|
+
|
64
|
+
def timestamp
|
65
|
+
@timestamp ||= Time.now.strftime("%Y%m%d%H%M%S")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|