dynamodb_model 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/docs/migrations-long.rb +72 -0
- data/docs/migrations-short.rb +29 -0
- data/lib/dynamodb_model/db_config.rb +60 -5
- data/lib/dynamodb_model/item.rb +98 -20
- data/lib/dynamodb_model/migration.rb +15 -4
- data/lib/dynamodb_model/migration/common.rb +84 -0
- data/lib/dynamodb_model/migration/dsl.rb +96 -73
- data/lib/dynamodb_model/migration/dsl/global_secondary_index.rb +71 -0
- data/lib/dynamodb_model/migration/executor.rb +30 -0
- data/lib/dynamodb_model/migration/generator.rb +7 -4
- data/lib/dynamodb_model/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e369807797f7eeb8eef8476828b0760a85e8076
|
4
|
+
data.tar.gz: 23f014fb766cda2c085bbc932ab9961946155150
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa48e2bf21ff32e98640a3264f35c2b9e77ae51d1e8638c912404041fc68b1da336b24c3005dc703457ff013eec49fea9234ea516ac1473d611b90dc5f59cd06
|
7
|
+
data.tar.gz: eaebb86f390ce9e2035af5e50f71f3235c5459284ee4cf540425272f0d511b826b568ad925da367fa9b47c2749e950bac56692901f805a3247cb31cc9e43818e
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
|
5
|
+
|
6
|
+
## [0.3.0]
|
7
|
+
- DSL methods now available: create_table, update_table
|
8
|
+
- Also can add GSI indexes within update_table with: i.gsi
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class CreateCommentsMigration < DynamodbModel::Migration
|
2
|
+
def up
|
3
|
+
create_table :comments do |t|
|
4
|
+
t.partition_key "post_id:string" # required
|
5
|
+
t.sort_key "created_at:string" # optional
|
6
|
+
t.provisioned_throughput(5) # sets both read and write, defaults to 5 when not set
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class UpdateCommentsMigration < DynamodbModel::Migration
|
12
|
+
def up
|
13
|
+
update_table :comments do |t|
|
14
|
+
|
15
|
+
# t.global_secondary_index do
|
16
|
+
# t.gsi(METHOD, INDEX_NAME) do
|
17
|
+
|
18
|
+
# You normally create an index like so:
|
19
|
+
#
|
20
|
+
# t.gsi(:create) do |i|
|
21
|
+
# i.partition_key = "post_id:string" # partition_key is required
|
22
|
+
# i.sort_key = "updated_at:string" # sort_key is optional
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# The index name will be inferred from the partition_key and sort_key when
|
26
|
+
# not explicitly set. Examples:
|
27
|
+
#
|
28
|
+
# index_name = "#{partition_key}-#{sort_key}-index"
|
29
|
+
# index_name = "post_id-index" # no sort key
|
30
|
+
# index_name = "post_id-updated_at-index" # has sort key
|
31
|
+
#
|
32
|
+
# The inference allows you to not have to worry about the index
|
33
|
+
# naming scheme. You can still set the index_name explicitly like so:
|
34
|
+
#
|
35
|
+
# t.gsi(:create, "post_id-updated_at-index") do |i|
|
36
|
+
# i.partition_key = "post_id:string" # partition_key is required
|
37
|
+
# i.sort_key = "updated_at:string" # sort_key is optional
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
t.gsi(:create) do |i|
|
41
|
+
i.partition_key "post_id:string"
|
42
|
+
i.sort_key "updated_at:string" # optional
|
43
|
+
|
44
|
+
# translates to
|
45
|
+
# i.key_schema({...})
|
46
|
+
# also makes sure that the schema_keys are added to the attributes_definitions
|
47
|
+
|
48
|
+
# t.projected_attributes(:all) # default if not called
|
49
|
+
# t.projected_attributes(:keys_only) # other ways to call
|
50
|
+
# t.projected_attributes([:id, :body, :tags, :updated_at])
|
51
|
+
# translates to:
|
52
|
+
# Valid Values: ALL | KEYS_ONLY | INCLUDE
|
53
|
+
# t.projection(
|
54
|
+
# projection_type: :all, # defaults to all
|
55
|
+
# )
|
56
|
+
# t.projection(
|
57
|
+
# projection_type: :include, # defaults to all
|
58
|
+
# non_key_attributes: [:id, :body, :tags, :updated_at], # defaults to all
|
59
|
+
# )
|
60
|
+
|
61
|
+
i.provisioned_throughput(10)
|
62
|
+
end
|
63
|
+
|
64
|
+
t.gsi(:update, "category-index") do |i|
|
65
|
+
i.provisioned_throughput(10)
|
66
|
+
end
|
67
|
+
|
68
|
+
t.gsi(:delete, "category-index")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class CreateCommentsMigration < DynamodbModel::Migration
|
2
|
+
def up
|
3
|
+
create_table :comments do |t|
|
4
|
+
t.partition_key "post_id:string" # required
|
5
|
+
t.sort_key "created_at:string" # optional
|
6
|
+
t.provisioned_throughput(5) # sets both read and write, defaults to 5 when not set
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class UpdateCommentsMigration < DynamodbModel::Migration
|
12
|
+
def up
|
13
|
+
update_table :comments do |t|
|
14
|
+
t.gsi(:create) do |i|
|
15
|
+
i.partition_key "post_id:string"
|
16
|
+
i.sort_key "updated_at:string" # optional
|
17
|
+
|
18
|
+
i.provisioned_throughput(10)
|
19
|
+
end
|
20
|
+
|
21
|
+
t.gsi(:update, "update-me-index") do |i|
|
22
|
+
i.provisioned_throughput(10)
|
23
|
+
end
|
24
|
+
|
25
|
+
t.gsi(:delete, "delete-me-index")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -12,6 +12,11 @@ module DynamodbModel::DbConfig
|
|
12
12
|
self.class.db
|
13
13
|
end
|
14
14
|
|
15
|
+
# NOTE: Class including DynamodbModel::DbConfig is required to have table_name method defined
|
16
|
+
def namespaced_table_name
|
17
|
+
[self.class.table_namespace, table_name].reject {|s| s.nil? || s.empty?}.join('-')
|
18
|
+
end
|
19
|
+
|
15
20
|
module ClassMethods
|
16
21
|
@@db = nil
|
17
22
|
def db
|
@@ -30,21 +35,71 @@ module DynamodbModel::DbConfig
|
|
30
35
|
end
|
31
36
|
|
32
37
|
def db_config
|
38
|
+
return @db_config if @db_config
|
39
|
+
|
33
40
|
if defined?(Jets)
|
34
|
-
|
41
|
+
config_path = "#{Jets.root}config/dynamodb.yml"
|
42
|
+
env = Jets.env
|
35
43
|
else
|
36
44
|
config_path = ENV['DYNAMODB_MODEL_CONFIG'] || "./config/dynamodb.yml"
|
37
45
|
env = ENV['DYNAMODB_MODEL_ENV'] || "development"
|
38
|
-
|
46
|
+
end
|
47
|
+
|
48
|
+
config = YAML.load(erb_result(config_path))
|
49
|
+
@db_config ||= config[env] || {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def table_namespace(*args)
|
53
|
+
case args.size
|
54
|
+
when 0
|
55
|
+
get_table_namespace
|
56
|
+
when 1
|
57
|
+
set_table_namespace(args[0])
|
39
58
|
end
|
40
59
|
end
|
41
60
|
|
42
|
-
|
43
|
-
|
44
|
-
return @table_namespace if @table_namespace
|
61
|
+
def get_table_namespace
|
62
|
+
return @table_namespace if defined?(@table_namespace)
|
45
63
|
|
46
64
|
config = db_config
|
47
65
|
@table_namespace = config['table_namespace']
|
48
66
|
end
|
67
|
+
|
68
|
+
def set_table_namespace(value)
|
69
|
+
@table_namespace = value
|
70
|
+
end
|
71
|
+
|
72
|
+
def erb_result(path)
|
73
|
+
template = IO.read(path)
|
74
|
+
begin
|
75
|
+
ERB.new(template, nil, "-").result(binding)
|
76
|
+
rescue Exception => e
|
77
|
+
puts e
|
78
|
+
puts e.backtrace if ENV['DEBUG']
|
79
|
+
|
80
|
+
# how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051
|
81
|
+
# syntax errors have the (erb):xxx info in e.message
|
82
|
+
# undefined variables have (erb):xxx info in e.backtrac
|
83
|
+
error_info = e.message.split("\n").grep(/\(erb\)/)[0]
|
84
|
+
error_info ||= e.backtrace.grep(/\(erb\)/)[0]
|
85
|
+
raise unless error_info # unable to find the (erb):xxx: error line
|
86
|
+
line = error_info.split(':')[1].to_i
|
87
|
+
puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '').colorize(:green)}"
|
88
|
+
|
89
|
+
template_lines = template.split("\n")
|
90
|
+
context = 5 # lines of context
|
91
|
+
top, bottom = [line-context-1, 0].max, line+context-1
|
92
|
+
spacing = template_lines.size.to_s.size
|
93
|
+
template_lines[top..bottom].each_with_index do |line_content, index|
|
94
|
+
line_number = top+index+1
|
95
|
+
if line_number == line
|
96
|
+
printf("%#{spacing}d %s\n".colorize(:red), line_number, line_content)
|
97
|
+
else
|
98
|
+
printf("%#{spacing}d %s\n", line_number, line_content)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
exit 1 unless ENV['TEST']
|
102
|
+
end
|
103
|
+
end
|
49
104
|
end
|
50
105
|
end
|
data/lib/dynamodb_model/item.rb
CHANGED
@@ -8,25 +8,25 @@ require "yaml"
|
|
8
8
|
#
|
9
9
|
# Examples:
|
10
10
|
#
|
11
|
-
# post =
|
11
|
+
# post = MyModel.new
|
12
12
|
# post = post.replace(title: "test title")
|
13
13
|
#
|
14
14
|
# post.attrs[:id] now contain a generaetd unique partition_key id.
|
15
15
|
# Usually the partition_key is 'id'. You can set your own unique id also:
|
16
16
|
#
|
17
|
-
# post =
|
17
|
+
# post = MyModel.new(id: "myid", title: "my title")
|
18
18
|
# post.replace
|
19
19
|
#
|
20
20
|
# Note that the replace method replaces the entire item, so you
|
21
21
|
# need to merge the attributes if you want to keep the other attributes.
|
22
22
|
#
|
23
|
-
# post =
|
23
|
+
# post = MyModel.find("myid")
|
24
24
|
# post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
|
25
25
|
# post.replace
|
26
26
|
#
|
27
27
|
# The convenience `attrs` method performs a deep_merge:
|
28
28
|
#
|
29
|
-
# post =
|
29
|
+
# post = MyModel.find("myid")
|
30
30
|
# post.attrs("desc": "my desc") # <= does a deep_merge
|
31
31
|
# post.replace
|
32
32
|
#
|
@@ -100,10 +100,16 @@ module DynamodbModel
|
|
100
100
|
@attributes
|
101
101
|
end
|
102
102
|
|
103
|
-
#
|
104
|
-
#
|
103
|
+
# Adds very little wrapper logic to scan.
|
104
|
+
#
|
105
|
+
# * Automatically add table_name to options for convenience.
|
106
|
+
# * Decorates return value. Returns Array of [MyModel.new] instead of the
|
107
|
+
# dynamodb client response.
|
108
|
+
#
|
109
|
+
# Other than that, usage is same was using the dynamodb client scan method
|
110
|
+
# directly. Example:
|
105
111
|
#
|
106
|
-
#
|
112
|
+
# MyModel.scan(
|
107
113
|
# expression_attribute_names: {"#updated_at"=>"updated_at"},
|
108
114
|
# filter_expression: "#updated_at between :start_time and :end_time",
|
109
115
|
# expression_attribute_values: {
|
@@ -112,20 +118,78 @@ module DynamodbModel
|
|
112
118
|
# }
|
113
119
|
# )
|
114
120
|
#
|
115
|
-
#
|
116
|
-
#
|
117
|
-
# Post.scan(filter: "updated_at between :start_time and :end_time")
|
118
|
-
#
|
119
|
-
# which automatically maps the structure.
|
121
|
+
# AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
|
120
122
|
def self.scan(params={})
|
121
123
|
puts("Should not use scan for production. It's slow and expensive. You should create either a LSI or GSI and use query the index instead.")
|
124
|
+
params = { table_name: table_name }.merge(params)
|
125
|
+
resp = db.scan(params)
|
126
|
+
resp.items.map {|i| self.new(i) }
|
127
|
+
end
|
122
128
|
|
123
|
-
|
124
|
-
|
129
|
+
# Adds very little wrapper logic to query.
|
130
|
+
#
|
131
|
+
# * Automatically add table_name to options for convenience.
|
132
|
+
# * Decorates return value. Returns Array of [MyModel.new] instead of the
|
133
|
+
# dynamodb client response.
|
134
|
+
#
|
135
|
+
# Other than that, usage is same was using the dynamodb client query method
|
136
|
+
# directly. Example:
|
137
|
+
#
|
138
|
+
# MyModel.query(
|
139
|
+
# index_name: 'category-index',
|
140
|
+
# expression_attribute_names: { "#category_name" => "category" },
|
141
|
+
# expression_attribute_values: { ":category_value" => "Entertainment" },
|
142
|
+
# key_condition_expression: "#category_name = :category_value",
|
143
|
+
# )
|
144
|
+
#
|
145
|
+
# AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
|
146
|
+
def self.query(params={})
|
147
|
+
params = { table_name: table_name }.merge(params)
|
148
|
+
resp = db.query(params)
|
149
|
+
resp.items.map {|i| self.new(i) }
|
150
|
+
end
|
151
|
+
|
152
|
+
# Translates simple query searches:
|
153
|
+
#
|
154
|
+
# Post.where({category: "Drama"}, index_name: "category-index")
|
155
|
+
#
|
156
|
+
# translates to
|
157
|
+
#
|
158
|
+
# resp = db.query(
|
159
|
+
# table_name: "proj-dev-post",
|
160
|
+
# index_name: 'category-index',
|
161
|
+
# expression_attribute_names: { "#category_name" => "category" },
|
162
|
+
# expression_attribute_values: { ":category_value" => category },
|
163
|
+
# key_condition_expression: "#category_name = :category_value",
|
164
|
+
# )
|
165
|
+
#
|
166
|
+
# TODO: Implement nicer where syntax with index_name as a chained method.
|
167
|
+
#
|
168
|
+
# Post.where({category: "Drama"}, {index_name: "category-index"})
|
169
|
+
# VS
|
170
|
+
# Post.where(category: "Drama").index_name("category-index")
|
171
|
+
def self.where(attributes, options={})
|
172
|
+
raise "attributes.size == 1 only supported for now" if attributes.size != 1
|
173
|
+
|
174
|
+
attr_name = attributes.keys.first
|
175
|
+
attr_value = attributes[attr_name]
|
176
|
+
|
177
|
+
# params = {
|
178
|
+
# expression_attribute_names: { "#category_name" => "category" },
|
179
|
+
# expression_attribute_values: { ":category_value" => "Entertainment" },
|
180
|
+
# key_condition_expression: "#category_name = :category_value",
|
181
|
+
# }
|
182
|
+
name_key, value_key = "##{attr_name}_name", ":#{attr_name}_value"
|
183
|
+
params = {
|
184
|
+
expression_attribute_names: { name_key => attr_name },
|
185
|
+
expression_attribute_values: { value_key => attr_value },
|
186
|
+
key_condition_expression: "#{name_key} = #{value_key}",
|
125
187
|
}
|
126
|
-
params
|
127
|
-
|
128
|
-
|
188
|
+
# Allow direct access to override params passed to dynamodb query options.
|
189
|
+
# This is is how index_name is passed:
|
190
|
+
params = params.merge(options)
|
191
|
+
|
192
|
+
query(params)
|
129
193
|
end
|
130
194
|
|
131
195
|
def self.replace(attrs)
|
@@ -156,7 +220,7 @@ module DynamodbModel
|
|
156
220
|
key: {partition_key => id}
|
157
221
|
)
|
158
222
|
attributes = resp.item # unwraps the item's attributes
|
159
|
-
|
223
|
+
self.new(attributes) if attributes
|
160
224
|
end
|
161
225
|
|
162
226
|
# Two ways to use the delete method:
|
@@ -203,9 +267,23 @@ module DynamodbModel
|
|
203
267
|
end
|
204
268
|
end
|
205
269
|
|
206
|
-
def self.table_name
|
207
|
-
|
270
|
+
def self.table_name(*args)
|
271
|
+
case args.size
|
272
|
+
when 0
|
273
|
+
get_table_name
|
274
|
+
when 1
|
275
|
+
set_table_name(args[0])
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def self.get_table_name
|
280
|
+
@table_name ||= self.name.pluralize.underscore
|
208
281
|
[table_namespace, @table_name].reject {|s| s.nil? || s.empty?}.join('-')
|
209
282
|
end
|
283
|
+
|
284
|
+
def self.set_table_name(value)
|
285
|
+
@table_name = value
|
286
|
+
end
|
287
|
+
|
210
288
|
end
|
211
289
|
end
|
@@ -2,15 +2,26 @@ module DynamodbModel
|
|
2
2
|
class Migration
|
3
3
|
autoload :Dsl, "dynamodb_model/migration/dsl"
|
4
4
|
autoload :Generator, "dynamodb_model/migration/generator"
|
5
|
+
autoload :Executor, "dynamodb_model/migration/executor"
|
5
6
|
|
6
7
|
def up
|
7
8
|
puts "Should defined an up method for your migration: #{self.class.name}"
|
8
9
|
end
|
9
10
|
|
10
|
-
def create_table(table_name)
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
14
25
|
end
|
15
26
|
end
|
16
27
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class DynamodbModel::Migration::Dsl
|
2
|
+
module Common
|
3
|
+
# http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Types/KeySchemaElement.html
|
4
|
+
# partition_key is required
|
5
|
+
def partition_key(identifier)
|
6
|
+
@partition_key_identifier = identifier # for later use. useful for conventional_index_name
|
7
|
+
adjust_schema_and_attributes(identifier, "hash")
|
8
|
+
end
|
9
|
+
|
10
|
+
# sort_key is optional
|
11
|
+
def sort_key(identifier)
|
12
|
+
@sort_key_identifier = identifier # for later use. useful for conventional_index_name
|
13
|
+
adjust_schema_and_attributes(identifier, "range")
|
14
|
+
end
|
15
|
+
|
16
|
+
# Parameters:
|
17
|
+
# identifier: "id:string" or "id"
|
18
|
+
# key_type: "hash" or "range"
|
19
|
+
#
|
20
|
+
# Adjusts the parameters for create_table to add the
|
21
|
+
# partition_key and sort_key
|
22
|
+
def adjust_schema_and_attributes(identifier, key_type)
|
23
|
+
name, attribute_type = identifier.split(':')
|
24
|
+
attribute_type = "string" if attribute_type.nil?
|
25
|
+
|
26
|
+
partition_key = {
|
27
|
+
attribute_name: name,
|
28
|
+
key_type: key_type.upcase
|
29
|
+
}
|
30
|
+
@key_schema << partition_key
|
31
|
+
|
32
|
+
attribute_definition = {
|
33
|
+
attribute_name: name,
|
34
|
+
attribute_type: ATTRIBUTE_TYPE_MAP[attribute_type]
|
35
|
+
}
|
36
|
+
@attribute_definitions << attribute_definition
|
37
|
+
end
|
38
|
+
|
39
|
+
# t.provisioned_throughput(5) # both
|
40
|
+
# t.provisioned_throughput(:read, 5)
|
41
|
+
# t.provisioned_throughput(:write, 5)
|
42
|
+
# t.provisioned_throughput(:both, 5)
|
43
|
+
def provisioned_throughput(*params)
|
44
|
+
case params.size
|
45
|
+
when 0 # reader method
|
46
|
+
return @provisioned_throughput # early return
|
47
|
+
when 1
|
48
|
+
# @provisioned_throughput_set_called useful for update_table
|
49
|
+
# only provide a provisioned_throughput settings if explicitly called for update_table
|
50
|
+
@provisioned_throughput_set_called = true
|
51
|
+
arg = params[0]
|
52
|
+
if arg.is_a?(Hash)
|
53
|
+
# Case:
|
54
|
+
# provisioned_throughput(
|
55
|
+
# read_capacity_units: 10,
|
56
|
+
# write_capacity_units: 10
|
57
|
+
# )
|
58
|
+
@provisioned_throughput = arg # set directly
|
59
|
+
return # early return
|
60
|
+
else # assume parameter is an Integer
|
61
|
+
# Case: provisioned_throughput(10)
|
62
|
+
capacity_type = :both
|
63
|
+
capacity_units = arg
|
64
|
+
end
|
65
|
+
when 2
|
66
|
+
@provisioned_throughput_set_called = true
|
67
|
+
# Case: provisioned_throughput(:read, 5)
|
68
|
+
capacity_type, capacity_units = params
|
69
|
+
end
|
70
|
+
|
71
|
+
map = {
|
72
|
+
read: :read_capacity_units,
|
73
|
+
write: :write_capacity_units,
|
74
|
+
}
|
75
|
+
|
76
|
+
if capacity_type = :both
|
77
|
+
@provisioned_throughput[map[:read]] = capacity_units
|
78
|
+
@provisioned_throughput[map[:write]] = capacity_units
|
79
|
+
else
|
80
|
+
@provisioned_throughput[capacity_type] = capacity_units
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
class DynamodbModel::Migration
|
2
2
|
class Dsl
|
3
|
+
autoload :GlobalSecondaryIndex, "dynamodb_model/migration/dsl/global_secondary_index"
|
4
|
+
autoload :Common, "dynamodb_model/migration/common"
|
5
|
+
|
3
6
|
include DynamodbModel::DbConfig
|
7
|
+
include Common
|
4
8
|
|
5
9
|
ATTRIBUTE_TYPE_MAP = {
|
6
10
|
'string' => 'S',
|
@@ -12,100 +16,119 @@ class DynamodbModel::Migration
|
|
12
16
|
}
|
13
17
|
|
14
18
|
attr_accessor :key_schema, :attribute_definitions
|
15
|
-
|
16
|
-
def initialize(table_name)
|
19
|
+
attr_accessor :table_name
|
20
|
+
def initialize(method_name, table_name, &block)
|
21
|
+
@method_name = method_name
|
17
22
|
@table_name = table_name
|
18
|
-
@
|
23
|
+
@block = block
|
24
|
+
|
25
|
+
# Dsl fills in atttributes in as methods are called within the block.
|
26
|
+
# Attributes for both create_table and updated_table:
|
19
27
|
@attribute_definitions = []
|
20
28
|
@provisioned_throughput = {
|
21
|
-
read_capacity_units:
|
22
|
-
write_capacity_units:
|
29
|
+
read_capacity_units: 5,
|
30
|
+
write_capacity_units: 5
|
23
31
|
}
|
24
|
-
end
|
25
32
|
|
26
|
-
|
27
|
-
|
28
|
-
def partition_key(identifier)
|
29
|
-
adjust_schema_and_attributes(identifier, "hash")
|
30
|
-
end
|
33
|
+
# Attributes for create_table only:
|
34
|
+
@key_schema = []
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
adjust_schema_and_attributes(identifier, "range")
|
36
|
+
# Attributes for update_table only:
|
37
|
+
@gsi_indexes = []
|
35
38
|
end
|
36
39
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
name, attribute_type = identifier.split(':')
|
45
|
-
attribute_type = "string" if attribute_type.nil?
|
46
|
-
|
47
|
-
partition_key = {
|
48
|
-
attribute_name: name,
|
49
|
-
key_type: key_type.upcase
|
50
|
-
}
|
51
|
-
@key_schema << partition_key
|
52
|
-
|
53
|
-
attribute_definition = {
|
54
|
-
attribute_name: name,
|
55
|
-
attribute_type: ATTRIBUTE_TYPE_MAP[attribute_type]
|
56
|
-
}
|
57
|
-
@attribute_definitions << attribute_definition
|
40
|
+
# t.gsi(:create) do |i|
|
41
|
+
# i.partition_key = "category:string"
|
42
|
+
# i.sort_key = "created_at:string" # optional
|
43
|
+
# end
|
44
|
+
def gsi(action, index_name=nil, &block)
|
45
|
+
gsi_index = GlobalSecondaryIndex.new(action, index_name, &block)
|
46
|
+
@gsi_indexes << gsi_index # store @gsi_index for the parent Dsl to use
|
58
47
|
end
|
48
|
+
alias_method :global_secondary_index, :gsi
|
59
49
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
case params.size
|
66
|
-
when 2
|
67
|
-
capacity_type, capacity_units = params
|
68
|
-
when 1
|
69
|
-
arg = params[0]
|
70
|
-
if arg.is_a?(Hash)
|
71
|
-
@provisioned_throughput = arg # set directly
|
72
|
-
return
|
73
|
-
else # assume parameter is an Integer
|
74
|
-
capacity_type = :both
|
75
|
-
capacity_units = arg
|
76
|
-
end
|
77
|
-
when 0 # reader method
|
78
|
-
return @provisioned_throughput
|
79
|
-
end
|
50
|
+
def evaluate
|
51
|
+
return if @evaluated
|
52
|
+
@block.call(self) if @block
|
53
|
+
@evaluated = true
|
54
|
+
end
|
80
55
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
56
|
+
# http://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/dynamo-example-create-table.html
|
57
|
+
# build the params up from dsl in memory and provides params to the
|
58
|
+
# executor
|
59
|
+
def params
|
60
|
+
evaluate # lazy evaluation: wait until as long as possible before evaluating code block
|
85
61
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
62
|
+
# Not using send because think its clearer in this case
|
63
|
+
case @method_name
|
64
|
+
when :create_table
|
65
|
+
params_create_table
|
66
|
+
when :update_table
|
67
|
+
params_update_table
|
91
68
|
end
|
92
69
|
end
|
93
70
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
table_name: @table_name,
|
71
|
+
def params_create_table
|
72
|
+
{
|
73
|
+
table_name: namespaced_table_name,
|
98
74
|
key_schema: @key_schema,
|
99
75
|
attribute_definitions: @attribute_definitions,
|
100
76
|
provisioned_throughput: @provisioned_throughput
|
101
77
|
}
|
102
|
-
|
103
|
-
|
78
|
+
end
|
79
|
+
|
80
|
+
def params_update_table
|
81
|
+
params = {
|
82
|
+
table_name: namespaced_table_name,
|
83
|
+
# update table take values only some values for the "parent" table
|
84
|
+
attribute_definitions: gsi_create_attribute_definitions, # This is only a partial
|
85
|
+
# key_schema: @key_schema, # update_table does not handle key_schema for the "parent" table,
|
86
|
+
}
|
87
|
+
# only set "parent" table provisioned_throughput if user actually invoked
|
88
|
+
# it in the dsl
|
89
|
+
params[:provisioned_throughput] = @provisioned_throughput if @provisioned_throughput_set_called
|
90
|
+
params[:global_secondary_index_updates] = global_secondary_index_updates
|
91
|
+
params
|
92
|
+
end
|
104
93
|
|
105
|
-
|
106
|
-
|
107
|
-
|
94
|
+
# Goes thorugh all the gsi_indexes that have been built up in memory.
|
95
|
+
# Find the gsi object that creates an index and then grab the
|
96
|
+
# attribute_definitions from it.
|
97
|
+
def gsi_create_attribute_definitions
|
98
|
+
gsi = @gsi_indexes.find { |gsi| gsi.action == :create }
|
99
|
+
if gsi
|
100
|
+
gsi.evaluate # force early evaluate since we need the params to
|
101
|
+
# add: current_attribute_definitions + gsi_attrs
|
102
|
+
gsi_attrs = gsi.attribute_definitions
|
108
103
|
end
|
104
|
+
all_attrs = if gsi_attrs
|
105
|
+
current_attribute_definitions + gsi_attrs
|
106
|
+
else
|
107
|
+
current_attribute_definitions
|
108
|
+
end
|
109
|
+
all_attrs.uniq
|
110
|
+
end
|
111
|
+
|
112
|
+
# maps each gsi to the hash structure expected by dynamodb update_table
|
113
|
+
# under the global_secondary_index_updates key:
|
114
|
+
#
|
115
|
+
# { create: {...} }
|
116
|
+
# { update: {...} }
|
117
|
+
# { delete: {...} }
|
118
|
+
def global_secondary_index_updates
|
119
|
+
@gsi_indexes.map do |gsi|
|
120
|
+
{ gsi.action => gsi.params }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# >> resp = Post.db.describe_table(table_name: "proj-dev-posts")
|
125
|
+
# >> resp.table.attribute_definitions.map(&:to_h)
|
126
|
+
# => [{:attribute_name=>"id", :attribute_type=>"S"}]
|
127
|
+
def current_attribute_definitions
|
128
|
+
return @current_attribute_definitions if @current_attribute_definitions
|
129
|
+
|
130
|
+
resp = db.describe_table(table_name: namespaced_table_name)
|
131
|
+
@current_attribute_definitions = resp.table.attribute_definitions.map(&:to_h)
|
109
132
|
end
|
110
133
|
end
|
111
134
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class DynamodbModel::Migration::Dsl
|
2
|
+
class GlobalSecondaryIndex
|
3
|
+
include Common
|
4
|
+
|
5
|
+
ATTRIBUTE_TYPE_MAP = DynamodbModel::Migration::Dsl::ATTRIBUTE_TYPE_MAP
|
6
|
+
|
7
|
+
attr_accessor :action, :key_schema, :attribute_definitions
|
8
|
+
attr_accessor :index_name
|
9
|
+
def initialize(action, index_name=nil, &block)
|
10
|
+
@action = action.to_sym # :create, :update, :index
|
11
|
+
@index_name = index_name
|
12
|
+
@block = block
|
13
|
+
|
14
|
+
# Dsl fills these atttributes in as methods are called within
|
15
|
+
# the block
|
16
|
+
@key_schema = []
|
17
|
+
@attribute_definitions = []
|
18
|
+
# default provisioned_throughput
|
19
|
+
@provisioned_throughput = {
|
20
|
+
read_capacity_units: 5,
|
21
|
+
write_capacity_units: 5
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def index_name
|
26
|
+
@index_name || conventional_index_name
|
27
|
+
end
|
28
|
+
|
29
|
+
def conventional_index_name
|
30
|
+
# @partition_key_identifier and @sort_key_identifier are set as immediately
|
31
|
+
# when the partition_key and sort_key methods are called in the dsl block.
|
32
|
+
# Usually look like this:
|
33
|
+
#
|
34
|
+
# @partition_key_identifier: post_id:string
|
35
|
+
# @sort_key_identifier: updated_at:string
|
36
|
+
#
|
37
|
+
# We strip the :string portion in case it is provided
|
38
|
+
#
|
39
|
+
partition_key = @partition_key_identifier.split(':').first
|
40
|
+
sort_key = @sort_key_identifier.split(':').first if @sort_key_identifier
|
41
|
+
[partition_key, sort_key, "index"].compact.join('-')
|
42
|
+
end
|
43
|
+
|
44
|
+
def evaluate
|
45
|
+
return if @evaluated
|
46
|
+
@block.call(self) if @block
|
47
|
+
@evaluated = true
|
48
|
+
end
|
49
|
+
|
50
|
+
def params
|
51
|
+
evaluate # lazy evaluation: wait until as long as possible before evaluating code block
|
52
|
+
|
53
|
+
params = { index_name: index_name } # required for all actions
|
54
|
+
|
55
|
+
if @action == :create
|
56
|
+
params[:key_schema] = @key_schema # required for create action
|
57
|
+
# hardcode to ALL for now
|
58
|
+
params[:projection] = { # required
|
59
|
+
projection_type: "ALL", # accepts ALL, KEYS_ONLY, INCLUDE
|
60
|
+
# non_key_attributes: ["NonKeyAttributeName"],
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
if [:create, :update].include?(@action)
|
65
|
+
params[:provisioned_throughput] = @provisioned_throughput
|
66
|
+
end
|
67
|
+
|
68
|
+
params
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class DynamodbModel::Migration
|
2
|
+
class Executor
|
3
|
+
include DynamodbModel::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
|
@@ -12,21 +12,24 @@ class DynamodbModel::Migration
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def generate
|
15
|
-
puts "Generating migration" unless @options[:quiet]
|
15
|
+
puts "Generating migration for #{@table_name}" unless @options[:quiet]
|
16
16
|
return if @options[:noop]
|
17
17
|
create_migration
|
18
18
|
end
|
19
19
|
|
20
20
|
def create_migration
|
21
|
-
|
21
|
+
migration_relative_path = "db/migrate/#{migration_file_name}.rb"
|
22
|
+
migration_path = "#{DynamodbModel.root}#{migration_relative_path}"
|
22
23
|
dir = File.dirname(migration_path)
|
23
24
|
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
24
25
|
IO.write(migration_path, migration_code)
|
26
|
+
puts "Migration file created: #{migration_relative_path}. To run:"
|
27
|
+
puts " jets db migrate #{migration_relative_path}"
|
25
28
|
end
|
26
29
|
|
27
30
|
def migration_code
|
28
31
|
# @table_name already set
|
29
|
-
@migration_class_name =
|
32
|
+
@migration_class_name = "#{@table_name}_migration".classify
|
30
33
|
@partition_key = @options[:partition_key]
|
31
34
|
@sort_key = @options[:sort_key]
|
32
35
|
@provisioned_throughput = @options[:provisioned_throughput] || 5
|
@@ -39,7 +42,7 @@ class DynamodbModel::Migration
|
|
39
42
|
end
|
40
43
|
|
41
44
|
def timestamp
|
42
|
-
|
45
|
+
@timestamp ||= Time.now.strftime("%Y%m%d%H%M%S")
|
43
46
|
end
|
44
47
|
end
|
45
48
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamodb_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tung Nguyen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-11-
|
11
|
+
date: 2017-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -89,17 +89,23 @@ extra_rdoc_files: []
|
|
89
89
|
files:
|
90
90
|
- ".gitignore"
|
91
91
|
- ".rspec"
|
92
|
+
- CHANGELOG.md
|
92
93
|
- Gemfile
|
93
94
|
- README.md
|
94
95
|
- Rakefile
|
95
96
|
- bin/console
|
96
97
|
- bin/setup
|
98
|
+
- docs/migrations-long.rb
|
99
|
+
- docs/migrations-short.rb
|
97
100
|
- dynamodb_model.gemspec
|
98
101
|
- lib/dynamodb_model.rb
|
99
102
|
- lib/dynamodb_model/db_config.rb
|
100
103
|
- lib/dynamodb_model/item.rb
|
101
104
|
- lib/dynamodb_model/migration.rb
|
105
|
+
- lib/dynamodb_model/migration/common.rb
|
102
106
|
- lib/dynamodb_model/migration/dsl.rb
|
107
|
+
- lib/dynamodb_model/migration/dsl/global_secondary_index.rb
|
108
|
+
- lib/dynamodb_model/migration/executor.rb
|
103
109
|
- lib/dynamodb_model/migration/generator.rb
|
104
110
|
- lib/dynamodb_model/migration/template.rb
|
105
111
|
- lib/dynamodb_model/util.rb
|
@@ -123,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
129
|
version: '0'
|
124
130
|
requirements: []
|
125
131
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.
|
132
|
+
rubygems_version: 2.4.5
|
127
133
|
signing_key:
|
128
134
|
specification_version: 4
|
129
135
|
summary: ActiveRecord-ish Dynamodb Model
|