dynamodb_model 0.2.0 → 0.3.0
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 +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
|