dynamini 2.8.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8dc1f7436267794e86bd3ba925c347fe0dbf8536
4
- data.tar.gz: 202c5d8166d5dbcd95b57a3474533e38f869e9c6
3
+ metadata.gz: e6bc03a0efcc9da7a30ed72044a0e7733cae9dd6
4
+ data.tar.gz: 3f0d131c9dec2a3f1e8af932a6daf6e513eb9b62
5
5
  SHA512:
6
- metadata.gz: 028d80338df534f3560290e18108c1cafecbd97cc64f7b99e5920c5c05754334e2008efe8be13488d44b48e9eca5f63777136a5c9092a28ca5828a90d0647eda
7
- data.tar.gz: 8b0fe74c4f36d33adf2ae8940407e357443b7d5f81c17f3c3a45f89ae10afd19a9aa696ce6037c621819d9e383a9cec428145b2bf84d44faa64aece87ada7702
6
+ metadata.gz: bc65042442a651365977bd108350e7ba3d516009060696f33834ffad426903fa86e01b99e5ae9627c3b62b0d3dddbda7ca5ec2a77ac3e35bd2aa5ba96dc6d966
7
+ data.tar.gz: de6f8de47956820198714c9887238fd4eef803eae0dcd42907b310c57a5a9dcdc59408625011472b2fbfc4128d1efcadb8e7883fa2b183d5e2ef44da1a281f10
data/Gemfile.lock CHANGED
@@ -1,30 +1,29 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamini (2.8.1)
4
+ dynamini (2.9.0)
5
5
  activemodel (>= 3, < 5.0)
6
6
  aws-sdk (~> 2)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activemodel (4.2.9)
12
- activesupport (= 4.2.9)
11
+ activemodel (4.2.5.1)
12
+ activesupport (= 4.2.5.1)
13
13
  builder (~> 3.1)
14
- activesupport (4.2.9)
14
+ activesupport (4.2.5.1)
15
15
  i18n (~> 0.7)
16
+ json (~> 1.7, >= 1.7.7)
16
17
  minitest (~> 5.1)
17
18
  thread_safe (~> 0.3, >= 0.3.4)
18
19
  tzinfo (~> 1.1)
19
- aws-sdk (2.10.42)
20
- aws-sdk-resources (= 2.10.42)
21
- aws-sdk-core (2.10.42)
22
- aws-sigv4 (~> 1.0)
20
+ aws-sdk (2.2.22)
21
+ aws-sdk-resources (= 2.2.22)
22
+ aws-sdk-core (2.2.22)
23
23
  jmespath (~> 1.0)
24
- aws-sdk-resources (2.10.42)
25
- aws-sdk-core (= 2.10.42)
26
- aws-sigv4 (1.0.2)
27
- builder (3.2.3)
24
+ aws-sdk-resources (2.2.22)
25
+ aws-sdk-core (= 2.2.22)
26
+ builder (3.2.2)
28
27
  coderay (1.1.0)
29
28
  diff-lcs (1.2.5)
30
29
  ffi (1.9.10)
@@ -49,14 +48,15 @@ GEM
49
48
  guard-shell (0.7.1)
50
49
  guard (>= 2.0.0)
51
50
  guard-compat (~> 1.0)
52
- i18n (0.8.6)
53
- jmespath (1.3.1)
51
+ i18n (0.7.0)
52
+ jmespath (1.1.3)
53
+ json (1.8.3)
54
54
  listen (3.0.3)
55
55
  rb-fsevent (>= 0.9.3)
56
56
  rb-inotify (>= 0.9)
57
57
  lumberjack (1.0.9)
58
58
  method_source (0.8.2)
59
- minitest (5.10.3)
59
+ minitest (5.8.4)
60
60
  nenv (0.2.0)
61
61
  notiffany (0.0.8)
62
62
  nenv (~> 0.1)
@@ -85,8 +85,8 @@ GEM
85
85
  shellany (0.0.1)
86
86
  slop (3.6.0)
87
87
  thor (0.19.1)
88
- thread_safe (0.3.6)
89
- tzinfo (1.2.3)
88
+ thread_safe (0.3.5)
89
+ tzinfo (1.2.2)
90
90
  thread_safe (~> 0.1)
91
91
 
92
92
  PLATFORMS
@@ -99,6 +99,3 @@ DEPENDENCIES
99
99
  guard-shell
100
100
  pry (~> 0)
101
101
  rspec (~> 3)
102
-
103
- BUNDLED WITH
104
- 1.16.0.pre.2
data/README.md CHANGED
@@ -156,6 +156,7 @@ Query takes the following arguments:
156
156
  * :end (optional)
157
157
  * :limit (optional)
158
158
  * :scan_index_forward (optional - set to false to sort by range key in desc order)
159
+ * :index_name (to query a secondary index - see below)
159
160
 
160
161
  Here's how you'd use it to find daily temperature data for a given city, selecting for specific date ranges:
161
162
 
@@ -197,9 +198,64 @@ DailyWeather.query(hash_key: "Toronto", limit: 2)
197
198
  DailyWeather.query(hash_key: "Toronto", scan_index_forward: false)
198
199
  > [C, B, A]
199
200
  ```
201
+
202
+ ## Table Scans
203
+ Table scanning is a very expensive operation, and should not be undertaken without a good understanding of the read/write costs. As such, Dynamini doesn't implement the traditional ActiveRecord collection methods like .all or .where. Instead, you can .scan, which has an interface much closer to DynamoDB's native SDK method.
204
+
205
+ The following options are supported:
206
+
207
+ * consistent_read (default: false)
208
+ * start_key (hash key of first desired item, default will scan from beginning)
209
+ * index_name (if scanning a secondary index - see below)
210
+ * limit
211
+
212
+ These two options are to support paralellization of table scanning, see: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html
213
+ * segment
214
+ * total_segments
215
+
216
+ ```ruby
217
+ products_page_one = Product.scan(limit: 100)
218
+ products_page_one.found # [product, product...]
219
+ page_two = Product.scan(start_key: products_page_one.last_evaluated_key)
220
+ ```
221
+
222
+ ## Secondary Indices
223
+ To define a secondary index (so that you can .scan it or .query it), you can list them at the top of your Dynamini subclass. The index names have to match the names you've set up through the DynamoDB console. If your secondary index uses a range key, specify it here as well.
224
+
225
+ ```ruby
226
+ class Comment < Dynamini::Base
227
+ set_hash_key :id
228
+ set_range_key :comment_date
229
+ set_secondary_index :score_index
230
+ set_secondary_index :popularity_index, range_key: :popularity
231
+ end
232
+ ```
233
+ For more information on how and why to use secondary indices, see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SecondaryIndexes.html
234
+ ## Testing
235
+ We've included an optional in-memory test client, so you don't necessarily have to connect to a real Dynamo instance when running tests. You could also use this in your development environment if you don't have a real Dynamo instance yet, but the data saved to it won't persist through a server restart.
236
+
237
+ To activate this feature, just require the testing module:
238
+ ```ruby
239
+ require 'dynamini/testing'
240
+ ```
241
+ This module replaces all API calls Dynamini makes to AWS DynamoDB with calls to Dynamini::TestClient.
242
+
243
+ The test client will not reset its database unless you tell it to, like so:
244
+ ```ruby
245
+ Vehicle.client.reset
246
+ ```
247
+
248
+ So, for instance, to get Rspec working with your test suite the way your ActiveRecord model behaved, add these lines to your spec_helper.rb:
249
+ ```ruby
250
+ require 'dynamini/testing'
251
+
252
+ config.after(:each) {
253
+ Vehicle.client.reset # Large test suites will be very slow and unpredictable otherwise!
254
+ }
255
+ ```
200
256
 
201
257
  ## Batch Saving
202
- Dynamo allows batch saving, see: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
258
+ Dynamini implements DynamoDB's batch write operation, mapped to the .import method you might be used to from ActiveRecord. http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
203
259
 
204
260
  ```ruby
205
261
  class Product < Dynamini::Base
@@ -229,30 +285,6 @@ Product.find('qwerty').updated_at
229
285
 
230
286
  ````
231
287
 
232
-
233
- ## Testing
234
- We've included an optional in-memory test client, so you don't necessarily have to connect to a real Dynamo instance when running tests. You could also use this in your development environment if you don't have a real Dynamo instance yet, but the data saved to it won't persist through a server restart.
235
-
236
- To activate this feature, just require the testing module:
237
- ```ruby
238
- require 'dynamini/testing'
239
- ```
240
- This module replaces all API calls Dynamini makes to AWS DynamoDB with calls to Dynamini::TestClient.
241
-
242
- The test client will not reset its database unless you tell it to, like so:
243
- ```ruby
244
- Vehicle.client.reset
245
- ```
246
-
247
- So, for instance, to get Rspec working with your test suite the way your ActiveRecord model behaved, add these lines to your spec_helper.rb:
248
- ```ruby
249
- require 'dynamini/testing'
250
-
251
- config.after(:each) {
252
- Vehicle.client.reset # Large test suites will be very slow and unpredictable otherwise!
253
- }
254
- ```
255
-
256
288
  ## Things to remember
257
289
  * Since DynamoDB is schemaless, your model will respond to any method that looks like a reader, meaning model.foo will return nil.
258
290
  * You can also write any arbitrary attribute to your model.
data/dynamini.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'dynamini'
3
- s.version = '2.8.1'
3
+ s.version = '2.9.0'
4
4
  s.summary = 'DynamoDB interface'
5
5
  s.description = 'Lightweight DynamoDB interface gem designed as
6
6
  a drop-in replacement for ActiveRecord.
@@ -33,28 +33,64 @@ module Dynamini
33
33
  client.batch_write_item(options)
34
34
  end
35
35
 
36
+ def scan(options = {})
37
+ validate_scan_options(options)
38
+ response = dynamo_scan(options)
39
+ if options[:index_name]
40
+ last_evaluated_key = response.last_evaluated_key[secondary_index[options[:index_name]][:hash_key_name].to_s]
41
+ else
42
+ last_evaluated_key = response.last_evaluated_key[hash_key.to_s]
43
+ end
44
+ OpenStruct.new(
45
+ last_evaluated_key: last_evaluated_key,
46
+ items: response.items.map { |i| new(i.symbolize_keys, false) }
47
+ )
48
+ end
49
+
36
50
  private
37
51
 
38
52
  def dynamo_batch_get(key_struct)
39
53
  client.batch_get_item(
40
- request_items: {
41
- table_name => {keys: key_struct}
42
- }
54
+ request_items: {
55
+ table_name => {keys: key_struct}
56
+ }
43
57
  )
44
58
  end
45
59
 
46
60
  def dynamo_batch_save(model_array)
47
61
  put_requests = model_array.map do |model|
48
62
  {
49
- put_request: {
50
- item: model.attributes.reject { |_k, v| v.blank? }.stringify_keys
51
- }
63
+ put_request: {
64
+ item: model.attributes.reject { |_k, v| v.blank? }.stringify_keys
65
+ }
52
66
  }
53
67
  end
54
68
  request_options = {
55
- request_items: {table_name => put_requests}
69
+ request_items: {table_name => put_requests}
56
70
  }
57
71
  client.batch_write_item(request_options)
58
72
  end
73
+
74
+ def dynamo_scan(options)
75
+ response = client.scan(
76
+ consistent_read: options[:consistent_read],
77
+ exclusive_start_key: options[:exclusive_start_key],
78
+ secondary_index_name: options[:index_name],
79
+ limit: options[:limit],
80
+ segment: options[:segment],
81
+ total_segments: options[:total_segments],
82
+ table_name: table_name
83
+ )
84
+ end
85
+
86
+ def validate_scan_options(options)
87
+ if options[:total_segments] && !options[:segment]
88
+ raise ArgumentError, 'Must specify segment if specifying total_segments'
89
+ elsif options[:segment] && !options[:total_segments]
90
+ raise ArgumentError, 'Must specify total_segments if specifying segment'
91
+ elsif options[:index_name] && !self.secondary_index[options[:index_name]]
92
+ raise ArgumentError, "Secondary index of #{options[:index_name]} does not exist"
93
+ end
94
+ end
59
95
  end
60
96
  end
@@ -84,6 +84,56 @@ module Dynamini
84
84
  OpenStruct.new(responses: responses)
85
85
  end
86
86
 
87
+ def scan(args = {})
88
+ records = get_table(args[:table_name]).values
89
+
90
+ sort_scanned_records!(records, args[:secondary_index_name]) if args[:secondary_index_name]
91
+ start_index = index_of_start_key(args, records)
92
+ items = limit_scanned_records(args[:limit], records, start_index)
93
+ last_evaluated_key = get_last_evaluated_key(args[:secondary_index_name], items)
94
+ OpenStruct.new(items: items, last_evaluated_key: last_evaluated_key)
95
+ end
96
+
97
+ def sort_scanned_records!(records, secondary_index_name)
98
+ index = secondary_index[secondary_index_name]
99
+ records.sort! do |a, b|
100
+ a[get_secondary_hash_key(index)] <=> b[get_secondary_hash_key(index)]
101
+ end
102
+ end
103
+
104
+ def index_of_start_key(args, records)
105
+ if args[:exclusive_start_key]
106
+ sec_index = secondary_index[args[:secondary_index_name]]
107
+ start_index = records.index do |r|
108
+ if sec_index
109
+ r[get_secondary_hash_key(sec_index)] == args[:exclusive_start_key]
110
+ else
111
+ r[hash_key_attr] == args[:exclusive_start_key]
112
+ end
113
+ end
114
+ start_index || -1
115
+ else
116
+ 0
117
+ end
118
+ end
119
+
120
+ def limit_scanned_records(limit, records, start_index)
121
+ end_index = limit ? start_index + limit - 1 : -1
122
+ records[start_index..end_index]
123
+ end
124
+
125
+ def get_last_evaluated_key(secondary_index_name, items)
126
+ # TODO should last evaluated key be present if the args[:limit] was reached?
127
+ # if items.length > records.length
128
+ index = secondary_index[secondary_index_name]
129
+ if index
130
+ { get_secondary_hash_key(index).to_s => items.last[get_secondary_hash_key(index)] }
131
+ else
132
+ { hash_key_attr.to_s => items.last[hash_key_attr] }
133
+ end
134
+ # end
135
+ end
136
+
87
137
  # TODO add range key support for delete, not currently implemented batch_operations.batch_delete
88
138
  def batch_write_item(request_options)
89
139
  request_options[:request_items].each do |table_name, requests|
@@ -126,5 +126,90 @@ describe Dynamini::BatchOperations do
126
126
  expect{ Dynamini::Base.find('7890') }.to raise_error(Dynamini::RecordNotFound)
127
127
  end
128
128
  end
129
+
130
+ class SecBase < Dynamini::Base
131
+ set_hash_key :id
132
+ set_secondary_index :sec, hash_key: :sec
133
+ set_secondary_index :rev, hash_key: :sec
134
+ end
135
+
136
+ describe '.scan' do
137
+
138
+ before do
139
+ SecBase.create(id: '123', sec: 'D')
140
+ SecBase.create(id: '124', sec: 'C')
141
+ SecBase.create(id: '125', sec: 'B')
142
+ SecBase.create(id: '126', sec: 'A')
143
+ end
144
+
145
+ context 'scanning the primary key' do
146
+ context 'with an exclusive_start_key' do
147
+ context 'with a limit' do
148
+ it 'retrieves the correct items' do
149
+ response = SecBase.scan(exclusive_start_key: '124', limit: 1)
150
+ expect(response.items.map { |i| i.id }).to eq(['124'])
151
+ expect(response.last_evaluated_key).to eq('124')
152
+ end
153
+ end
154
+ context 'without a limit' do
155
+ it 'retrieves the correct items' do
156
+ response = SecBase.scan(exclusive_start_key: '124')
157
+ expect(response.items.map { |i| i.id }).to eq(%w(124 125 126))
158
+ expect(response.last_evaluated_key).to eq('126')
159
+ end
160
+ end
161
+ end
162
+ context 'without an exclusive_start_key' do
163
+ context 'with a limit' do
164
+ it 'retrieves the correct items' do
165
+ response = SecBase.scan(limit: 2)
166
+ expect(response.items.map { |i| i.id }).to eq(%w(123 124))
167
+ expect(response.last_evaluated_key).to eq('124')
168
+ end
169
+ end
170
+ context 'without a limit' do
171
+ it 'retrieves the correct items' do
172
+ response = SecBase.scan
173
+ expect(response.items.map { |i| i.id }).to eq(%w(123 124 125 126))
174
+ expect(response.last_evaluated_key).to eq('126')
175
+ end
176
+ end
177
+ end
178
+ end
179
+ context 'scanning a secondary index' do
180
+ context 'with an exclusive_start_key' do
181
+ context 'with a limit' do
182
+ it 'retrieves the correct items' do
183
+ response = SecBase.scan(index_name: 'sec', exclusive_start_key: 'B', limit: 2)
184
+ expect(response.items.map { |i| i.sec }).to eq(%w(B C))
185
+ expect(response.last_evaluated_key).to eq('C')
186
+ end
187
+ end
188
+ context 'without a limit' do
189
+ it 'retrieves the correct items' do
190
+ response = SecBase.scan(index_name: 'sec', exclusive_start_key: 'B')
191
+ expect(response.items.map { |i| i.sec }).to eq(%w(B C D))
192
+ expect(response.last_evaluated_key).to eq('D')
193
+ end
194
+ end
195
+ end
196
+ context 'without an exclusive_start_key' do
197
+ context 'with a limit' do
198
+ it 'retrieves the correct items' do
199
+ response = SecBase.scan(index_name: 'sec', limit: 3)
200
+ expect(response.items.map { |i| i.sec }).to eq(%w(A B C))
201
+ expect(response.last_evaluated_key).to eq('C')
202
+ end
203
+ end
204
+ context 'without a limit' do
205
+ it 'retrieves the correct items' do
206
+ response = SecBase.scan(index_name: 'sec')
207
+ expect(response.items.map { |i| i.sec }).to eq(%w(A B C D))
208
+ expect(response.last_evaluated_key).to eq('D')
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
129
214
  end
130
215
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamini
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.1
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Ward
@@ -15,7 +15,7 @@ authors:
15
15
  autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
- date: 2017-09-11 00:00:00.000000000 Z
18
+ date: 2017-09-12 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: activemodel