dynamoid 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Dynamoid.gemspec +3 -3
- data/README.markdown +72 -7
- data/VERSION +1 -1
- data/lib/dynamoid/adapter.rb +115 -23
- data/lib/dynamoid/adapter/aws_sdk.rb +50 -7
- data/lib/dynamoid/config.rb +1 -1
- data/lib/dynamoid/criteria.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +76 -4
- data/lib/dynamoid/document.rb +6 -1
- data/lib/dynamoid/fields.rb +17 -1
- data/lib/dynamoid/persistence.rb +22 -6
- data/spec/app/models/address.rb +1 -0
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +198 -1
- data/spec/dynamoid/adapter_spec.rb +55 -2
- data/spec/dynamoid/criteria/chain_spec.rb +23 -0
- data/spec/dynamoid/document_spec.rb +3 -3
- data/spec/dynamoid/fields_spec.rb +4 -4
- data/spec/dynamoid/persistence_spec.rb +46 -0
- metadata +4 -4
data/Dynamoid.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "dynamoid"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.6.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Josh Symonds"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-12-19"
|
13
13
|
s.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
|
14
14
|
s.email = "josh@joshsymonds.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -139,7 +139,7 @@ Gem::Specification.new do |s|
|
|
139
139
|
s.homepage = "http://github.com/Veraticus/Dynamoid"
|
140
140
|
s.licenses = ["MIT"]
|
141
141
|
s.require_paths = ["lib"]
|
142
|
-
s.rubygems_version = "1.8.
|
142
|
+
s.rubygems_version = "1.8.24"
|
143
143
|
s.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
|
144
144
|
|
145
145
|
if s.respond_to? :specification_version then
|
data/README.markdown
CHANGED
@@ -17,15 +17,72 @@ Installing Dynamoid is pretty simple. First include the Gem in your Gemfile:
|
|
17
17
|
```ruby
|
18
18
|
gem 'dynamoid'
|
19
19
|
```
|
20
|
+
## Prerequisities
|
20
21
|
|
21
|
-
|
22
|
+
Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (1.6.9), rails 3.2.8.
|
23
|
+
Hence the configuration as needed for aws to work will be dealt with by aws setup.
|
24
|
+
|
25
|
+
Here are the steps to setup aws-sdk.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem 'aws-sdk'
|
29
|
+
```
|
30
|
+
(or) include the aws-sdk in your Gemfile.
|
31
|
+
|
32
|
+
|
33
|
+
[Refer this link for aws setup](https://github.com/amazonwebservices/aws-sdk-for-ruby)
|
34
|
+
|
35
|
+
1. Just like the config/database.yml this file requires an entry for each environment, create config/aws.yml as follows:
|
36
|
+
|
37
|
+
Fill in your AWS Access Key ID and Secret Access Key
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
|
41
|
+
|
42
|
+
development:
|
43
|
+
access_key_id: REPLACE_WITH_ACCESS_KEY_ID
|
44
|
+
secret_access_key: REPLACE_WITH_SECRET_ACCESS_KEY
|
45
|
+
dynamodb_end_point: dynamodb.ap-southeast-1.amazonaws.com
|
46
|
+
|
47
|
+
test:
|
48
|
+
<<: *development
|
49
|
+
|
50
|
+
production:
|
51
|
+
<<: *development
|
52
|
+
|
53
|
+
```
|
54
|
+
|
55
|
+
(or)
|
56
|
+
|
57
|
+
|
58
|
+
2. Create config/initializers/aws.rb as follows:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
|
62
|
+
#Additionally include any of the dynamodb paramters as needed.
|
63
|
+
#(eg: if you would like to change the dynamodb endpoint, then add the parameter in
|
64
|
+
# in the file aws.yml or aws.rb
|
65
|
+
|
66
|
+
dynamo_db_endpoint : dynamodb.ap-southeast-1.amazonaws.com)
|
67
|
+
|
68
|
+
AWS.config({
|
69
|
+
:access_key_id => 'REPLACE_WITH_ACCESS_KEY_ID',
|
70
|
+
:secret_access_key => 'REPLACE_WITH_SECRET_ACCESS_KEY',
|
71
|
+
:dynamodb_end_point => dynamodb.ap-southeast-1.amazonaws.com
|
72
|
+
})
|
73
|
+
|
74
|
+
|
75
|
+
```
|
76
|
+
|
77
|
+
Refer code in Module: AWS, and from the link below for the other configuration options supported for dynamodb.
|
78
|
+
|
79
|
+
[Module AWS](http://docs.amazonwebservices.com/AWSRubySDK/latest/frames.html#!http%3A//docs.amazonwebservices.com/AWSRubySDK/latest/AWS.html)
|
80
|
+
|
81
|
+
Then you need to initialize Dynamoid config to get it going. Put code similar to this somewhere (a Rails initializer would be a great place for this if you're using Rails):
|
22
82
|
|
23
83
|
```ruby
|
24
84
|
Dynamoid.configure do |config|
|
25
|
-
|
26
|
-
# config.access_key = 'access_key' # If connecting to DynamoDB, your access key is required.
|
27
|
-
# config.secret_key = 'secret_key' # So is your secret key.
|
28
|
-
# config.endpoint = 'dynamodb.us-east-1.amazonaws.com' # Set the regional endpoint for DynamoDB.
|
85
|
+
config.adapter = 'aws_sdk' # This adapter establishes a connection to the DynamoDB servers using Amazon's own AWS gem.
|
29
86
|
config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have.
|
30
87
|
config.warn_on_scan = true # Output a warning to the logger when you perform a scan rather than a query on a table.
|
31
88
|
config.partitioning = true # Spread writes randomly across the database. See "partitioning" below for more.
|
@@ -252,14 +309,22 @@ Dynamoid borrows code, structure, and even its name very liberally from the trul
|
|
252
309
|
|
253
310
|
Also, without contributors the project wouldn't be nearly as awesome. So many thanks to:
|
254
311
|
|
312
|
+
* [Logan Bowers](https://github.com/loganb)
|
313
|
+
* [Lane LaRue](https://github.com/luxx)
|
314
|
+
* [Craig Heneveld](https://github.com/cheneveld)
|
255
315
|
* [Anantha Kumaran](https://github.com/ananthakumaran)
|
256
316
|
* [Jason Dew](https://github.com/jasondew)
|
257
317
|
|
258
318
|
## Running the tests
|
259
319
|
|
260
|
-
Running the tests is fairly simple. In one window, run `fake_dynamo`, and in the other, use `rake`.
|
320
|
+
Running the tests is fairly simple. In one window, run `fake_dynamo --port 4567`, and in the other, use `rake`.
|
261
321
|
|
262
322
|
## Copyright
|
263
323
|
|
264
|
-
Copyright (c) 2012 Josh Symonds.
|
324
|
+
Copyright (c) 2012 Josh Symonds.
|
325
|
+
|
326
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
327
|
+
|
328
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
265
329
|
|
330
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
data/lib/dynamoid/adapter.rb
CHANGED
@@ -45,16 +45,17 @@ module Dynamoid
|
|
45
45
|
#
|
46
46
|
# @param [String] table the name of the table to write the object to
|
47
47
|
# @param [Object] object the object itself
|
48
|
+
# @param [Hash] options Options that are passed to the put_item call
|
48
49
|
#
|
49
50
|
# @return [Object] the persisted object
|
50
51
|
#
|
51
52
|
# @since 0.2.0
|
52
|
-
def write(table, object)
|
53
|
+
def write(table, object, options = nil)
|
53
54
|
if Dynamoid::Config.partitioning? && object[:id]
|
54
55
|
object[:id] = "#{object[:id]}.#{Random.rand(Dynamoid::Config.partition_size)}"
|
55
56
|
object[:updated_at] = Time.now.to_f
|
56
57
|
end
|
57
|
-
put_item(table, object)
|
58
|
+
put_item(table, object, options)
|
58
59
|
end
|
59
60
|
|
60
61
|
# Read one or many keys from the selected table. This method intelligently calls batch_get or get on the underlying adapter depending on
|
@@ -73,7 +74,7 @@ module Dynamoid
|
|
73
74
|
ids = ids.collect{|id| range_key ? [id, range_key] : id}
|
74
75
|
if Dynamoid::Config.partitioning?
|
75
76
|
results = batch_get_item(table => id_with_partitions(ids))
|
76
|
-
{table => result_for_partition(results[table])}
|
77
|
+
{table => result_for_partition(results[table],table)}
|
77
78
|
else
|
78
79
|
batch_get_item(table => ids)
|
79
80
|
end
|
@@ -81,7 +82,7 @@ module Dynamoid
|
|
81
82
|
if Dynamoid::Config.partitioning?
|
82
83
|
ids = range_key ? [[ids, range_key]] : ids
|
83
84
|
results = batch_get_item(table => id_with_partitions(ids))
|
84
|
-
result_for_partition(results[table]).first
|
85
|
+
result_for_partition(results[table],table).first
|
85
86
|
else
|
86
87
|
get_item(table, ids, options)
|
87
88
|
end
|
@@ -91,15 +92,31 @@ module Dynamoid
|
|
91
92
|
# Delete an item from a table. If partitioning is turned on, deletes all partitioned keys as well.
|
92
93
|
#
|
93
94
|
# @param [String] table the name of the table to write the object to
|
94
|
-
# @param [
|
95
|
-
# @param [
|
95
|
+
# @param [Array] ids to delete, can also be a string of just one id
|
96
|
+
# @param [Array] range_key of the record to delete, can also be a string of just one range_key
|
96
97
|
#
|
97
|
-
|
98
|
-
|
99
|
-
if
|
100
|
-
|
98
|
+
def delete(table, ids, options = {})
|
99
|
+
range_key = options[:range_key] #array of range keys that matches the ids passed in
|
100
|
+
if ids.respond_to?(:each)
|
101
|
+
if range_key.respond_to?(:each)
|
102
|
+
#turn ids into array of arrays each element being hash_key, range_key
|
103
|
+
ids = ids.each_with_index.map{|id,i| [id,range_key[i]]}
|
104
|
+
else
|
105
|
+
ids = range_key ? [[ids, range_key]] : ids
|
106
|
+
end
|
107
|
+
|
108
|
+
if Dynamoid::Config.partitioning?
|
109
|
+
batch_delete_item(table => id_with_partitions(ids))
|
110
|
+
else
|
111
|
+
batch_delete_item(table => ids)
|
112
|
+
end
|
101
113
|
else
|
102
|
-
|
114
|
+
if Dynamoid::Config.partitioning?
|
115
|
+
ids = range_key ? [[ids, range_key]] : ids
|
116
|
+
batch_delete_item(table => id_with_partitions(ids))
|
117
|
+
else
|
118
|
+
delete_item(table, ids, options)
|
119
|
+
end
|
103
120
|
end
|
104
121
|
end
|
105
122
|
|
@@ -112,7 +129,7 @@ module Dynamoid
|
|
112
129
|
def scan(table, query, opts = {})
|
113
130
|
if Dynamoid::Config.partitioning?
|
114
131
|
results = benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
115
|
-
result_for_partition(results)
|
132
|
+
result_for_partition(results,table)
|
116
133
|
else
|
117
134
|
benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
118
135
|
end
|
@@ -141,24 +158,59 @@ module Dynamoid
|
|
141
158
|
def id_with_partitions(ids)
|
142
159
|
Array(ids).collect {|id| (0...Dynamoid::Config.partition_size).collect{|n| id.is_a?(Array) ? ["#{id.first}.#{n}", id.last] : "#{id}.#{n}"}}.flatten(1)
|
143
160
|
end
|
161
|
+
|
162
|
+
#Get original id (hash_key) and partiton number from a hash_key
|
163
|
+
#
|
164
|
+
# @param [String] id the id or hash_key of a record, ex. xxxxx.13
|
165
|
+
#
|
166
|
+
# @return [String,String] original_id and the partition number, ex original_id = xxxxx partition = 13
|
167
|
+
def get_original_id_and_partition id
|
168
|
+
partition = id.split('.').last
|
169
|
+
id = id.split(".#{partition}").first
|
170
|
+
|
171
|
+
return id, partition
|
172
|
+
end
|
144
173
|
|
145
|
-
# Takes an array of results that are partitioned, find the most recently updated
|
174
|
+
# Takes an array of query results that are partitioned, find the most recently updated ones that share an id and range_key, and return only the most recently updated. Compares each result by
|
146
175
|
# their id and updated_at attributes; if the updated_at is the greatest, then it must be the correct result.
|
147
176
|
#
|
148
177
|
# @param [Array] returned partitioned results from a query
|
178
|
+
# @param [String] table_name the name of the table
|
149
179
|
#
|
150
180
|
# @since 0.2.0
|
151
|
-
def result_for_partition(results)
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
181
|
+
def result_for_partition(results, table_name)
|
182
|
+
table = Dynamoid::Adapter::AwsSdk.get_table(table_name)
|
183
|
+
|
184
|
+
if table.range_key
|
185
|
+
range_key_name = table.range_key.name.to_sym
|
186
|
+
|
187
|
+
final_hash = {}
|
188
|
+
|
189
|
+
results.each do |record|
|
190
|
+
test_record = final_hash[record[range_key_name]]
|
191
|
+
|
192
|
+
if test_record.nil? || ((record[range_key_name] == test_record[range_key_name]) && (record[:updated_at] > test_record[:updated_at]))
|
193
|
+
#get ride of our partition and put it in the array with the range key
|
194
|
+
record[:id], partition = get_original_id_and_partition record[:id]
|
195
|
+
final_hash[record[range_key_name]] = record
|
159
196
|
end
|
160
197
|
end
|
161
|
-
|
198
|
+
|
199
|
+
return final_hash.values
|
200
|
+
else
|
201
|
+
{}.tap do |hash|
|
202
|
+
Array(results).each do |result|
|
203
|
+
next if result.nil?
|
204
|
+
#Need to find the value of id with out the . and partition number
|
205
|
+
id, partition = get_original_id_and_partition result[:id]
|
206
|
+
|
207
|
+
if !hash[id] || (result[:updated_at] > hash[id][:updated_at])
|
208
|
+
result[:id] = id
|
209
|
+
hash[id] = result
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end.values
|
213
|
+
end
|
162
214
|
end
|
163
215
|
|
164
216
|
# Delegate all methods that aren't defind here to the underlying adapter.
|
@@ -168,7 +220,47 @@ module Dynamoid
|
|
168
220
|
return benchmark(method, *args) {adapter.send(method, *args, &block)} if @adapter.respond_to?(method)
|
169
221
|
super
|
170
222
|
end
|
223
|
+
|
224
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
225
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
226
|
+
# one range key to the hash. If paritioning is on, will run a query for every parition and join the results
|
227
|
+
#
|
228
|
+
# @param [String] table_name the name of the table
|
229
|
+
# @param [Hash] opts the options to query the table with
|
230
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
231
|
+
# @option opts [Range] :range_value find the range key within this range
|
232
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
233
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
234
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
235
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
236
|
+
#
|
237
|
+
# @return [Array] an array of all matching items
|
238
|
+
#
|
239
|
+
def query(table_name, opts = {})
|
240
|
+
|
241
|
+
unless Dynamoid::Config.partitioning?
|
242
|
+
#no paritioning? just pass to the standard query method
|
243
|
+
Dynamoid::Adapter::AwsSdk.query(table_name, opts)
|
244
|
+
else
|
245
|
+
#get all the hash_values that could be possible
|
246
|
+
ids = id_with_partitions(opts[:hash_value])
|
171
247
|
|
172
|
-
|
248
|
+
#lets not overwrite with the original options
|
249
|
+
modified_options = opts.clone
|
250
|
+
results = []
|
251
|
+
|
252
|
+
#loop and query on each of the partition ids
|
253
|
+
ids.each do |id|
|
254
|
+
modified_options[:hash_value] = id
|
255
|
+
|
256
|
+
query_result = Dynamoid::Adapter::AwsSdk.query(table_name, modified_options)
|
257
|
+
query_result = [query_result] if !query_result.is_a?(Array)
|
173
258
|
|
259
|
+
results = results + query_result unless query_result.nil?
|
260
|
+
end
|
261
|
+
|
262
|
+
result_for_partition results, table_name
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
174
266
|
end
|
@@ -15,10 +15,30 @@ module Dynamoid
|
|
15
15
|
# Establish the connection to DynamoDB.
|
16
16
|
#
|
17
17
|
# @return [AWS::DynamoDB::Connection] the raw DynamoDB connection
|
18
|
-
#
|
18
|
+
# Call DynamoDB new, with no parameters.
|
19
|
+
# Make sure the aws.yml file or aws.rb file, refer the link for more details.
|
20
|
+
#https://github.com/amazonwebservices/aws-sdk-for-ruby
|
21
|
+
# 1. Create config/aws.yml as follows:
|
22
|
+
# Fill in your AWS Access Key ID and Secret Access Key
|
23
|
+
# http://aws.amazon.com/security-credentials
|
24
|
+
#access_key_id: REPLACE_WITH_ACCESS_KEY_ID
|
25
|
+
#secret_access_key: REPLACE_WITH_SECRET_ACCESS_KEY
|
26
|
+
#(or)
|
27
|
+
#2, Create config/initializers/aws.rb as follows:
|
28
|
+
# load the libraries
|
29
|
+
#require 'aws'
|
30
|
+
# log requests using the default rails logger
|
31
|
+
#AWS.config(:logger => Rails.logger)
|
32
|
+
# load credentials from a file
|
33
|
+
#config_path = File.expand_path(File.dirname(__FILE__)+"/../aws.yml")
|
34
|
+
#AWS.config(YAML.load(File.read(config_path)))
|
35
|
+
#Additionally include any of the dynamodb paramters as needed
|
36
|
+
#(eg: if you would like to change the dynamodb endpoint, then add the parameter in
|
37
|
+
# the following paramter in the file aws.yml or aws.rb
|
38
|
+
# dynamo_db_endpoint : dynamodb.ap-southeast-1.amazonaws.com)
|
19
39
|
# @since 0.2.0
|
20
40
|
def connect!
|
21
|
-
|
41
|
+
@@connection = AWS::DynamoDB.new
|
22
42
|
end
|
23
43
|
|
24
44
|
# Return the established connection.
|
@@ -54,6 +74,29 @@ module Dynamoid
|
|
54
74
|
end
|
55
75
|
hash
|
56
76
|
end
|
77
|
+
|
78
|
+
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
79
|
+
#
|
80
|
+
# @example Delete IDs 1 and 2 from the table testtable
|
81
|
+
# Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => ['1', '2'])
|
82
|
+
#or
|
83
|
+
# Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
|
84
|
+
#
|
85
|
+
# @param [Hash] options the hash of tables and IDs to delete
|
86
|
+
#
|
87
|
+
# @return nil
|
88
|
+
#
|
89
|
+
def batch_delete_item(options)
|
90
|
+
return nil if options.all?{|k, v| v.empty?}
|
91
|
+
options.each do |t, ids|
|
92
|
+
Array(ids).in_groups_of(25, false) do |group|
|
93
|
+
batch = AWS::DynamoDB::BatchWrite.new(:config => @@connection.config)
|
94
|
+
batch.delete(t,group)
|
95
|
+
batch.process!
|
96
|
+
end
|
97
|
+
end
|
98
|
+
nil
|
99
|
+
end
|
57
100
|
|
58
101
|
# Create a table on DynamoDB. This usually takes a long time to complete.
|
59
102
|
#
|
@@ -111,9 +154,6 @@ module Dynamoid
|
|
111
154
|
# @return [Hash] a hash representing the raw item in DynamoDB
|
112
155
|
#
|
113
156
|
# @since 0.2.0
|
114
|
-
|
115
|
-
|
116
|
-
|
117
157
|
def get_item(table_name, key, options = {})
|
118
158
|
range_key = options.delete(:range_key)
|
119
159
|
table = get_table(table_name)
|
@@ -150,9 +190,12 @@ module Dynamoid
|
|
150
190
|
# @param [Object] object a hash or Dynamoid object to persist
|
151
191
|
#
|
152
192
|
# @since 0.2.0
|
153
|
-
def put_item(table_name, object)
|
193
|
+
def put_item(table_name, object, options = nil)
|
154
194
|
table = get_table(table_name)
|
155
|
-
table.items.create(
|
195
|
+
table.items.create(
|
196
|
+
object.delete_if{|k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?)},
|
197
|
+
options || {}
|
198
|
+
)
|
156
199
|
end
|
157
200
|
|
158
201
|
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
data/lib/dynamoid/config.rb
CHANGED
@@ -23,7 +23,7 @@ module Dynamoid
|
|
23
23
|
option :partition_size, :default => 200
|
24
24
|
option :endpoint, :default => 'dynamodb.us-east-1.amazonaws.com'
|
25
25
|
option :use_ssl, :default => true
|
26
|
-
option :port, :default => '
|
26
|
+
option :port, :default => '443'
|
27
27
|
option :included_models, :default => []
|
28
28
|
option :identity_map, :default => false
|
29
29
|
|
data/lib/dynamoid/criteria.rb
CHANGED
@@ -9,7 +9,7 @@ module Dynamoid
|
|
9
9
|
|
10
10
|
module ClassMethods
|
11
11
|
|
12
|
-
[:where, :all, :first, :each, :limit, :start].each do |meth|
|
12
|
+
[:where, :all, :first, :each, :limit, :start, :scan_index_forward].each do |meth|
|
13
13
|
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
14
14
|
# see Dynamoid::Criteria::Chain.
|
15
15
|
#
|
@@ -16,6 +16,7 @@ module Dynamoid #:nodoc:
|
|
16
16
|
@query = {}
|
17
17
|
@source = source
|
18
18
|
@consistent_read = false
|
19
|
+
@scan_index_forward = true
|
19
20
|
end
|
20
21
|
|
21
22
|
# The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
|
@@ -45,6 +46,63 @@ module Dynamoid #:nodoc:
|
|
45
46
|
def all
|
46
47
|
records
|
47
48
|
end
|
49
|
+
|
50
|
+
# Destroys all the records matching the criteria.
|
51
|
+
#
|
52
|
+
def destroy_all
|
53
|
+
ids = []
|
54
|
+
|
55
|
+
if range?
|
56
|
+
ranges = []
|
57
|
+
Dynamoid::Adapter.query(source.table_name, range_query).collect do |hash|
|
58
|
+
ids << hash[source.hash_key.to_sym]
|
59
|
+
ranges << hash[source.range_key.to_sym]
|
60
|
+
end
|
61
|
+
|
62
|
+
Dynamoid::Adapter.delete(source.table_name, ids,{:range_key => ranges})
|
63
|
+
elsif index
|
64
|
+
#TODO: test this throughly and find a way to delete all index table records for one source record
|
65
|
+
if index.range_key?
|
66
|
+
results = Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts))
|
67
|
+
else
|
68
|
+
results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
results.collect do |hash|
|
72
|
+
ids << hash[source.hash_key.to_sym]
|
73
|
+
index_ranges << hash[source.range_key.to_sym]
|
74
|
+
end
|
75
|
+
|
76
|
+
unless ids.nil? || ids.empty?
|
77
|
+
ids = ids.to_a
|
78
|
+
|
79
|
+
if @start
|
80
|
+
ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
|
81
|
+
index_ranges = index_ranges.drop_while { |range| range != @start.hash_key }.drop(1) unless index_ranges.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
if @limit
|
85
|
+
ids = ids.take(@limit)
|
86
|
+
index_ranges = index_ranges.take(@limit)
|
87
|
+
end
|
88
|
+
|
89
|
+
Dynamoid::Adapter.delete(source.table_name, ids)
|
90
|
+
|
91
|
+
if index.range_key?
|
92
|
+
Dynamoid::Adapter.delete(index.table_name, ids,{:range_key => index_ranges})
|
93
|
+
else
|
94
|
+
Dynamoid::Adapter.delete(index.table_name, ids)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
else
|
99
|
+
Dynamoid::Adapter.scan(source.table_name, query, scan_opts).collect do |hash|
|
100
|
+
ids << hash[source.hash_key.to_sym]
|
101
|
+
end
|
102
|
+
|
103
|
+
Dynamoid::Adapter.delete(source.table_name, ids)
|
104
|
+
end
|
105
|
+
end
|
48
106
|
|
49
107
|
# Returns the first record matching the criteria.
|
50
108
|
#
|
@@ -63,6 +121,11 @@ module Dynamoid #:nodoc:
|
|
63
121
|
self
|
64
122
|
end
|
65
123
|
|
124
|
+
def scan_index_forward(scan_index_forward)
|
125
|
+
@scan_index_forward = scan_index_forward
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
66
129
|
# Allows you to use the results of a search as an enumerable over the results found.
|
67
130
|
#
|
68
131
|
# @since 0.2.0
|
@@ -145,7 +208,7 @@ module Dynamoid #:nodoc:
|
|
145
208
|
raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
|
146
209
|
end
|
147
210
|
|
148
|
-
Dynamoid::Adapter.scan(source.table_name, query,
|
211
|
+
Dynamoid::Adapter.scan(source.table_name, query, scan_opts).collect {|hash| source.from_database(hash) }
|
149
212
|
end
|
150
213
|
|
151
214
|
# Format the provided query so that it can be used to query results from DynamoDB.
|
@@ -190,7 +253,7 @@ module Dynamoid #:nodoc:
|
|
190
253
|
def range_query
|
191
254
|
opts = { :hash_value => query[source.hash_key] }
|
192
255
|
if key = query.keys.find { |k| k.to_s.include?('.') }
|
193
|
-
opts.merge!(
|
256
|
+
opts.merge!(range_hash(key))
|
194
257
|
end
|
195
258
|
opts.merge(query_opts).merge(consistent_opts)
|
196
259
|
end
|
@@ -214,15 +277,24 @@ module Dynamoid #:nodoc:
|
|
214
277
|
end
|
215
278
|
|
216
279
|
def start_key
|
217
|
-
|
280
|
+
hash_key_type = @start.class.attributes[@start.class.hash_key][:type] == :string ? 'S' : 'N'
|
281
|
+
key = { :hash_key_element => { hash_key_type => @start.hash_key.to_s } }
|
218
282
|
if range_key = @start.class.range_key
|
219
283
|
range_key_type = @start.class.attributes[range_key][:type] == :string ? 'S' : 'N'
|
220
|
-
key.merge!({:range_key_element => { range_key_type => @start.send(range_key) } })
|
284
|
+
key.merge!({:range_key_element => { range_key_type => @start.send(range_key).to_s } })
|
221
285
|
end
|
222
286
|
key
|
223
287
|
end
|
224
288
|
|
225
289
|
def query_opts
|
290
|
+
opts = {}
|
291
|
+
opts[:limit] = @limit if @limit
|
292
|
+
opts[:next_token] = start_key if @start
|
293
|
+
opts[:scan_index_forward] = @scan_index_forward
|
294
|
+
opts
|
295
|
+
end
|
296
|
+
|
297
|
+
def scan_opts
|
226
298
|
opts = {}
|
227
299
|
opts[:limit] = @limit if @limit
|
228
300
|
opts[:next_token] = start_key if @start
|
data/lib/dynamoid/document.rb
CHANGED
@@ -8,8 +8,9 @@ module Dynamoid #:nodoc:
|
|
8
8
|
include Dynamoid::Components
|
9
9
|
|
10
10
|
included do
|
11
|
-
class_attribute :options
|
11
|
+
class_attribute :options, :read_only_attributes
|
12
12
|
self.options = {}
|
13
|
+
self.read_only_attributes = []
|
13
14
|
|
14
15
|
Dynamoid::Config.included_models << self
|
15
16
|
end
|
@@ -29,6 +30,10 @@ module Dynamoid #:nodoc:
|
|
29
30
|
self.options = options
|
30
31
|
end
|
31
32
|
|
33
|
+
def attr_readonly(*read_only_attributes)
|
34
|
+
self.read_only_attributes.concat read_only_attributes.map(&:to_s)
|
35
|
+
end
|
36
|
+
|
32
37
|
# Returns the read_capacity for this table.
|
33
38
|
#
|
34
39
|
# @since 0.4.0
|
data/lib/dynamoid/fields.rb
CHANGED
@@ -81,7 +81,23 @@ module Dynamoid #:nodoc:
|
|
81
81
|
# @since 0.2.0
|
82
82
|
def update_attributes(attributes)
|
83
83
|
attributes.each {|attribute, value| self.write_attribute(attribute, value)}
|
84
|
-
save
|
84
|
+
if self.new_record # if never saved save.
|
85
|
+
save
|
86
|
+
else # update attributes if we have saved.
|
87
|
+
# next if self.read_only_attributes.include? attribute.to_s put this back in.
|
88
|
+
run_callbacks(:save) do
|
89
|
+
update! do |u|
|
90
|
+
attributes.each do |attribute, value|
|
91
|
+
u.set attribute => dump_field(
|
92
|
+
self.read_attribute(attribute),
|
93
|
+
self.class.attributes[attribute.to_sym]
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
save
|
100
|
+
end
|
85
101
|
end
|
86
102
|
|
87
103
|
# Update a single attribute, saving the object afterwards.
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -17,7 +17,7 @@ module Dynamoid
|
|
17
17
|
#
|
18
18
|
# @since 0.2.0
|
19
19
|
def table_name
|
20
|
-
"#{Dynamoid::Config.namespace}_#{options[:name] ? options[:name] : self.name.downcase.pluralize}"
|
20
|
+
"#{Dynamoid::Config.namespace}_#{options[:name] ? options[:name] : self.name.split('::').last.downcase.pluralize}"
|
21
21
|
end
|
22
22
|
|
23
23
|
# Creates a table.
|
@@ -53,7 +53,7 @@ module Dynamoid
|
|
53
53
|
#
|
54
54
|
# @since 0.2.0
|
55
55
|
def table_exists?(table_name)
|
56
|
-
Dynamoid::Adapter.tables.include?(table_name)
|
56
|
+
Dynamoid::Adapter.tables ? Dynamoid::Adapter.tables.include?(table_name) : false
|
57
57
|
end
|
58
58
|
|
59
59
|
def from_database(attrs = {})
|
@@ -109,6 +109,15 @@ module Dynamoid
|
|
109
109
|
else
|
110
110
|
value
|
111
111
|
end
|
112
|
+
when :boolean
|
113
|
+
# persisted as 't', but because undump is called during initialize it can come in as true
|
114
|
+
if value == 't' || value == true
|
115
|
+
true
|
116
|
+
elsif value == 'f' || value == false
|
117
|
+
false
|
118
|
+
else
|
119
|
+
raise ArgumentError, "Boolean column neither true nor false"
|
120
|
+
end
|
112
121
|
end
|
113
122
|
end
|
114
123
|
|
@@ -146,9 +155,12 @@ module Dynamoid
|
|
146
155
|
# @since 0.2.0
|
147
156
|
def save(options = {})
|
148
157
|
self.class.create_table
|
149
|
-
|
158
|
+
|
150
159
|
if new_record?
|
151
|
-
|
160
|
+
conditions = { :unless_exists => [self.class.hash_key]}
|
161
|
+
conditions[:unless_exists] << range_key if(range_key)
|
162
|
+
|
163
|
+
run_callbacks(:create) { persist(conditions) }
|
152
164
|
else
|
153
165
|
persist
|
154
166
|
end
|
@@ -225,6 +237,10 @@ module Dynamoid
|
|
225
237
|
value.to_time.to_f
|
226
238
|
when :serialized
|
227
239
|
options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
|
240
|
+
when :boolean
|
241
|
+
value.to_s[0]
|
242
|
+
else
|
243
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
228
244
|
end
|
229
245
|
end
|
230
246
|
|
@@ -232,10 +248,10 @@ module Dynamoid
|
|
232
248
|
# save its indexes.
|
233
249
|
#
|
234
250
|
# @since 0.2.0
|
235
|
-
def persist
|
251
|
+
def persist(conditions = nil)
|
236
252
|
run_callbacks(:save) do
|
237
253
|
self.hash_key = SecureRandom.uuid if self.hash_key.nil? || self.hash_key.blank?
|
238
|
-
Dynamoid::Adapter.write(self.class.table_name, self.dump)
|
254
|
+
Dynamoid::Adapter.write(self.class.table_name, self.dump, conditions)
|
239
255
|
save_indexes
|
240
256
|
@new_record = false
|
241
257
|
true
|
data/spec/app/models/address.rb
CHANGED
@@ -16,11 +16,12 @@ describe Dynamoid::Adapter::AwsSdk do
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
context 'with a preexisting table' do
|
19
|
+
context 'with a preexisting table without paritioning' do
|
20
20
|
before(:all) do
|
21
21
|
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable1', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable1')
|
22
22
|
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable2', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable2')
|
23
23
|
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable3', :id, :range_key => { :range => :number }) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable3')
|
24
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable4', :id, :range_key => { :range => :number }) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable4')
|
24
25
|
end
|
25
26
|
|
26
27
|
# GetItem, PutItem and DeleteItem
|
@@ -99,6 +100,56 @@ describe Dynamoid::Adapter::AwsSdk do
|
|
99
100
|
results['dynamoid_tests_TestTable3'].should include({:name => 'Josh', :id => '1', :range => 1.0})
|
100
101
|
results['dynamoid_tests_TestTable3'].should include({:name => 'Justin', :id => '2', :range => 2.0})
|
101
102
|
end
|
103
|
+
|
104
|
+
# BatchDeleteItem
|
105
|
+
it "performs BatchDeleteItem with singular keys" do
|
106
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
107
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable2', {:id => '1', :name => 'Justin'})
|
108
|
+
|
109
|
+
Dynamoid::Adapter.batch_delete_item('dynamoid_tests_TestTable1' => ['1'], 'dynamoid_tests_TestTable2' => ['1'])
|
110
|
+
|
111
|
+
results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => '1', 'dynamoid_tests_TestTable2' => '1')
|
112
|
+
results.size.should == 0
|
113
|
+
|
114
|
+
results['dynamoid_tests_TestTable1'].should_not include({:name => 'Josh', :id => '1'})
|
115
|
+
results['dynamoid_tests_TestTable2'].should_not include({:name => 'Justin', :id => '1'})
|
116
|
+
end
|
117
|
+
|
118
|
+
it "performs BatchDeleteItem with multiple keys" do
|
119
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
120
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
|
121
|
+
|
122
|
+
Dynamoid::Adapter.batch_delete_item('dynamoid_tests_TestTable1' => ['1', '2'])
|
123
|
+
|
124
|
+
results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => ['1', '2'])
|
125
|
+
results.size.should == 0
|
126
|
+
|
127
|
+
results['dynamoid_tests_TestTable1'].should_not include({:name => 'Josh', :id => '1'})
|
128
|
+
results['dynamoid_tests_TestTable1'].should_not include({:name => 'Justin', :id => '2'})
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'performs BatchDeleteItem with one ranged key' do
|
132
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :name => 'Josh', :range => 1.0})
|
133
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '2', :name => 'Justin', :range => 2.0})
|
134
|
+
|
135
|
+
Dynamoid::Adapter.batch_delete_item('dynamoid_tests_TestTable3' => [['1', 1.0]])
|
136
|
+
results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable3' => [['1', 1.0]])
|
137
|
+
results.size.should == 0
|
138
|
+
|
139
|
+
results['dynamoid_tests_TestTable3'].should_not include({:name => 'Josh', :id => '1', :range => 1.0})
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'performs BatchDeleteItem with multiple ranged keys' do
|
143
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :name => 'Josh', :range => 1.0})
|
144
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '2', :name => 'Justin', :range => 2.0})
|
145
|
+
|
146
|
+
Dynamoid::Adapter.batch_delete_item('dynamoid_tests_TestTable3' => [['1', 1.0],['2', 2.0]])
|
147
|
+
results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable3' => [['1', 1.0],['2', 2.0]])
|
148
|
+
results.size.should == 0
|
149
|
+
|
150
|
+
results['dynamoid_tests_TestTable3'].should_not include({:name => 'Josh', :id => '1', :range => 1.0})
|
151
|
+
results['dynamoid_tests_TestTable3'].should_not include({:name => 'Justin', :id => '2', :range => 2.0})
|
152
|
+
end
|
102
153
|
|
103
154
|
# ListTables
|
104
155
|
it 'performs ListTables' do
|
@@ -174,6 +225,152 @@ describe Dynamoid::Adapter::AwsSdk do
|
|
174
225
|
|
175
226
|
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', {}).should include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"})
|
176
227
|
end
|
228
|
+
|
229
|
+
context 'correct ordering ' do
|
230
|
+
before do
|
231
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1', :order => 1, :range => 1.0})
|
232
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1', :order => 2, :range => 2.0})
|
233
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1', :order => 3, :range => 3.0})
|
234
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1', :order => 4, :range => 4.0})
|
235
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1', :order => 5, :range => 5.0})
|
236
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1', :order => 6, :range => 6.0})
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'performs query on a table with a range and selects items less than that is in the correct order, scan_index_forward true' do
|
240
|
+
query = Dynamoid::Adapter.query('dynamoid_tests_TestTable4', :hash_value => '1', :range_greater_than => 0, :scan_index_forward => true)
|
241
|
+
query[0].should == {:id => '1', :order => 1, :range => BigDecimal.new(1)}
|
242
|
+
query[1].should == {:id => '1', :order => 2, :range => BigDecimal.new(2)}
|
243
|
+
query[2].should == {:id => '1', :order => 3, :range => BigDecimal.new(3)}
|
244
|
+
query[3].should == {:id => '1', :order => 4, :range => BigDecimal.new(4)}
|
245
|
+
query[4].should == {:id => '1', :order => 5, :range => BigDecimal.new(5)}
|
246
|
+
query[5].should == {:id => '1', :order => 6, :range => BigDecimal.new(6)}
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'performs query on a table with a range and selects items less than that is in the correct order, scan_index_forward false' do
|
250
|
+
query = Dynamoid::Adapter.query('dynamoid_tests_TestTable4', :hash_value => '1', :range_greater_than => 0, :scan_index_forward => false)
|
251
|
+
query[5].should == {:id => '1', :order => 1, :range => BigDecimal.new(1)}
|
252
|
+
query[4].should == {:id => '1', :order => 2, :range => BigDecimal.new(2)}
|
253
|
+
query[3].should == {:id => '1', :order => 3, :range => BigDecimal.new(3)}
|
254
|
+
query[2].should == {:id => '1', :order => 4, :range => BigDecimal.new(4)}
|
255
|
+
query[1].should == {:id => '1', :order => 5, :range => BigDecimal.new(5)}
|
256
|
+
query[0].should == {:id => '1', :order => 6, :range => BigDecimal.new(6)}
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context 'with a preexisting table with paritioning' do
|
262
|
+
before(:all) do
|
263
|
+
@previous_value = Dynamoid::Config.partitioning
|
264
|
+
Dynamoid::Config.partitioning = true
|
265
|
+
|
266
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable1', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable1')
|
267
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable2', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable2')
|
268
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable3', :id, :range_key => { :range => :number }) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable3')
|
269
|
+
end
|
270
|
+
|
271
|
+
after(:all) do
|
272
|
+
Dynamoid::Config.partitioning = @previous_value
|
273
|
+
end
|
274
|
+
|
275
|
+
# Query
|
276
|
+
it 'performs query on a table and returns items' do
|
277
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1.1', :name => 'Josh'})
|
278
|
+
|
279
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable1', :hash_value => '1').first.should == { :id=> '1', :name=>"Josh" }
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'performs query on a table and returns items if there are multiple items' do
|
283
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1.1', :name => 'Josh'})
|
284
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2.1', :name => 'Justin'})
|
285
|
+
|
286
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable1', :hash_value => '1').first.should == { :id=> '1', :name=>"Josh" }
|
287
|
+
end
|
288
|
+
|
289
|
+
context 'range queries' do
|
290
|
+
before do
|
291
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1.1', :range => 1.0})
|
292
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1.1', :range => 3.0})
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'performs query on a table with a range and selects items in a range' do
|
296
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_value => 0.0..3.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}]
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'performs query on a table with a range and selects items greater than' do
|
300
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_greater_than => 1.0).should =~ [{:id => '1', :range => BigDecimal.new(3)}]
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'performs query on a table with a range and selects items less than' do
|
304
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_less_than => 2.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}]
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'performs query on a table with a range and selects items gte' do
|
308
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_gte => 1.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}]
|
309
|
+
end
|
310
|
+
|
311
|
+
it 'performs query on a table with a range and selects items lte' do
|
312
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_lte => 3.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# Scan
|
317
|
+
it 'performs scan on a table and returns items' do
|
318
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1.1', :name => 'Josh'})
|
319
|
+
|
320
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{ :id=> '1', :name=>"Josh" }]
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'performs scan on a table and returns items if there are multiple items but only one match' do
|
324
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1.1', :name => 'Josh'})
|
325
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2.1', :name => 'Justin'})
|
326
|
+
|
327
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{ :id=> '1', :name=>"Josh" }]
|
328
|
+
end
|
329
|
+
|
330
|
+
it 'performs scan on a table and returns multiple items if there are multiple matches' do
|
331
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1.1', :name => 'Josh'})
|
332
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2.1', :name => 'Josh'})
|
333
|
+
|
334
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"})
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'performs scan on a table and returns all items if no criteria are specified' do
|
338
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1.1', :name => 'Josh'})
|
339
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2.1', :name => 'Josh'})
|
340
|
+
|
341
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', {}).should include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"})
|
342
|
+
end
|
343
|
+
|
344
|
+
context 'correct ordering ' do
|
345
|
+
before do
|
346
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1.1', :range => 1.0})
|
347
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1.2', :range => 2.0})
|
348
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1.3', :range => 3.0})
|
349
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1.4', :range => 4.0})
|
350
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1.5', :range => 5.0})
|
351
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable4', {:id => '1.6', :range => 6.0})
|
352
|
+
end
|
353
|
+
|
354
|
+
it 'performs query on a table with a range and selects items less than that is in the correct order, scan_index_forward true' do
|
355
|
+
query = Dynamoid::Adapter.query('dynamoid_tests_TestTable4', :hash_value => '1', :range_greater_than => 0, :scan_index_forward => true)
|
356
|
+
query[0].should == {:id => '1', :range => BigDecimal.new(1)}
|
357
|
+
query[1].should == {:id => '1', :range => BigDecimal.new(2)}
|
358
|
+
query[2].should == {:id => '1', :range => BigDecimal.new(3)}
|
359
|
+
query[3].should == {:id => '1', :range => BigDecimal.new(4)}
|
360
|
+
query[4].should == {:id => '1', :range => BigDecimal.new(5)}
|
361
|
+
query[5].should == {:id => '1', :range => BigDecimal.new(6)}
|
362
|
+
end
|
363
|
+
|
364
|
+
it 'performs query on a table with a range and selects items less than that is in the correct order, scan_index_forward false' do
|
365
|
+
query = Dynamoid::Adapter.query('dynamoid_tests_TestTable4', :hash_value => '1', :range_greater_than => 0, :scan_index_forward => false)
|
366
|
+
query[5].should == {:id => '1', :range => BigDecimal.new(1)}
|
367
|
+
query[4].should == {:id => '1', :range => BigDecimal.new(2)}
|
368
|
+
query[3].should == {:id => '1', :range => BigDecimal.new(3)}
|
369
|
+
query[2].should == {:id => '1', :range => BigDecimal.new(4)}
|
370
|
+
query[1].should == {:id => '1', :range => BigDecimal.new(5)}
|
371
|
+
query[0].should == {:id => '1', :range => BigDecimal.new(6)}
|
372
|
+
end
|
373
|
+
end
|
177
374
|
end
|
178
375
|
|
179
376
|
# DescribeTable
|
@@ -25,7 +25,7 @@ describe "Dynamoid::Adapter" do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'writes through the adapter' do
|
28
|
-
Dynamoid::Adapter.expects(:put_item).with('dynamoid_tests_TestTable', {:id => '123'}).returns(true)
|
28
|
+
Dynamoid::Adapter.expects(:put_item).with('dynamoid_tests_TestTable', {:id => '123'}, nil).returns(true)
|
29
29
|
|
30
30
|
Dynamoid::Adapter.write('dynamoid_tests_TestTable', {:id => '123'})
|
31
31
|
end
|
@@ -42,6 +42,18 @@ describe "Dynamoid::Adapter" do
|
|
42
42
|
Dynamoid::Adapter.read('dynamoid_tests_TestTable', ['1', '2'])
|
43
43
|
end
|
44
44
|
|
45
|
+
it 'delete through the adapter for one ID' do
|
46
|
+
Dynamoid::Adapter.expects(:delete_item).with('dynamoid_tests_TestTable', '123', {}).returns(nil)
|
47
|
+
|
48
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', '123')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'deletes through the adapter for many IDs' do
|
52
|
+
Dynamoid::Adapter.expects(:batch_delete_item).with({'dynamoid_tests_TestTable' => ['1', '2']}).returns(nil)
|
53
|
+
|
54
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', ['1', '2'])
|
55
|
+
end
|
56
|
+
|
45
57
|
it 'reads through the adapter for one ID and a range key' do
|
46
58
|
Dynamoid::Adapter.expects(:get_item).with('dynamoid_tests_TestTable', '123', :range_key => 2.0).returns(true)
|
47
59
|
|
@@ -53,6 +65,18 @@ describe "Dynamoid::Adapter" do
|
|
53
65
|
|
54
66
|
Dynamoid::Adapter.read('dynamoid_tests_TestTable', ['1', '2'], :range_key => 2.0)
|
55
67
|
end
|
68
|
+
|
69
|
+
it 'deletes through the adapter for one ID and a range key' do
|
70
|
+
Dynamoid::Adapter.expects(:delete_item).with('dynamoid_tests_TestTable', '123', :range_key => 2.0).returns(nil)
|
71
|
+
|
72
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', '123', :range_key => 2.0)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'deletes through the adapter for many IDs and a range key' do
|
76
|
+
Dynamoid::Adapter.expects(:batch_delete_item).with({'dynamoid_tests_TestTable' => [['1', 2.0], ['2', 2.0]]}).returns(nil)
|
77
|
+
|
78
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', ['1', '2'], :range_key => [2.0,2.0])
|
79
|
+
end
|
56
80
|
end
|
57
81
|
|
58
82
|
context 'with partitioning' do
|
@@ -109,9 +133,38 @@ describe "Dynamoid::Adapter" do
|
|
109
133
|
@time = DateTime.now
|
110
134
|
@array =[{:id => '1.0', :updated_at => @time - 6.hours}, {:id => '1.1', :updated_at => @time - 3.hours}, {:id => '1.2', :updated_at => @time - 1.hour}, {:id => '1.3', :updated_at => @time - 6.hours}, {:id => '2.0', :updated_at => @time}]
|
111
135
|
|
112
|
-
Dynamoid::Adapter.result_for_partition(@array).should =~ [{:id => '1', :updated_at => @time - 1.hour}, {:id => '2', :updated_at => @time}]
|
136
|
+
Dynamoid::Adapter.result_for_partition(@array,"dynamoid_tests_TestTable").should =~ [{:id => '1', :updated_at => @time - 1.hour}, {:id => '2', :updated_at => @time}]
|
113
137
|
end
|
114
138
|
|
139
|
+
it 'returns a valid original id and partition number' do
|
140
|
+
@id = "12345.387327.-sdf3"
|
141
|
+
@partition_number = "4"
|
142
|
+
Dynamoid::Adapter.get_original_id_and_partition("#{@id}.#{@partition_number}").should == [@id, @partition_number]
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'delete through the adapter for one ID' do
|
146
|
+
Dynamoid::Adapter.expects(:batch_delete_item).with('dynamoid_tests_TestTable' => (0...Dynamoid::Config.partition_size).collect{|n| "123.#{n}"}).returns(nil)
|
147
|
+
|
148
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', '123')
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'deletes through the adapter for many IDs' do
|
152
|
+
Dynamoid::Adapter.expects(:batch_delete_item).with('dynamoid_tests_TestTable' => (0...Dynamoid::Config.partition_size).collect{|n| "1.#{n}"} + (0...Dynamoid::Config.partition_size).collect{|n| "2.#{n}"}).returns(nil)
|
153
|
+
|
154
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', ['1', '2'])
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'deletes through the adapter for one ID and a range key' do
|
158
|
+
Dynamoid::Adapter.expects(:batch_delete_item).with('dynamoid_tests_TestTable' => (0...Dynamoid::Config.partition_size).collect{|n| ["123.#{n}", 2.0]}).returns(nil)
|
159
|
+
|
160
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', '123', :range_key => 2.0)
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'deletes through the adapter for many IDs and a range key' do
|
164
|
+
Dynamoid::Adapter.expects(:batch_delete_item).with('dynamoid_tests_TestTable' => (0...Dynamoid::Config.partition_size).collect{|n| ["1.#{n}", 2.0]} + (0...Dynamoid::Config.partition_size).collect{|n| ["2.#{n}", 2.0]}).returns(nil)
|
165
|
+
|
166
|
+
Dynamoid::Adapter.delete('dynamoid_tests_TestTable', ['1', '2'], :range_key => [2.0,2.0])
|
167
|
+
end
|
115
168
|
end
|
116
169
|
|
117
170
|
end
|
@@ -136,5 +136,28 @@ describe "Dynamoid::Associations::Chain" do
|
|
136
136
|
@chain.send(:records_with_range).should == [@tweet3]
|
137
137
|
end
|
138
138
|
end
|
139
|
+
|
140
|
+
context 'destroy alls' do
|
141
|
+
before do
|
142
|
+
@tweet1 = Tweet.create(:tweet_id => "x", :group => "one")
|
143
|
+
@tweet2 = Tweet.create(:tweet_id => "x", :group => "two")
|
144
|
+
@tweet3 = Tweet.create(:tweet_id => "xx", :group => "two")
|
145
|
+
@chain = Dynamoid::Criteria::Chain.new(Tweet)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'destroys tweet with a range simple range query' do
|
149
|
+
@chain.query = { :tweet_id => "x" }
|
150
|
+
@chain.all.size.should == 2
|
151
|
+
@chain.destroy_all
|
152
|
+
@chain.consistent.all.size.should == 0
|
153
|
+
end
|
139
154
|
|
155
|
+
it 'deletes one specific tweet with range' do
|
156
|
+
@chain = Dynamoid::Criteria::Chain.new(Tweet)
|
157
|
+
@chain.query = { :tweet_id => "xx", :group => "two" }
|
158
|
+
@chain.all.size.should == 1
|
159
|
+
@chain.destroy_all
|
160
|
+
@chain.consistent.all.size.should == 0
|
161
|
+
end
|
162
|
+
end
|
140
163
|
end
|
@@ -6,7 +6,7 @@ describe "Dynamoid::Document" do
|
|
6
6
|
@address = Address.new
|
7
7
|
|
8
8
|
@address.new_record.should be_true
|
9
|
-
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>nil, :options=>nil}
|
9
|
+
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>nil, :options=>nil, :deliverable => nil}
|
10
10
|
end
|
11
11
|
|
12
12
|
it 'responds to will_change! methods for all fields' do
|
@@ -22,7 +22,7 @@ describe "Dynamoid::Document" do
|
|
22
22
|
|
23
23
|
@address.new_record.should be_true
|
24
24
|
|
25
|
-
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>"Chicago", :options=>nil}
|
25
|
+
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>"Chicago", :options=>nil, :deliverable => nil}
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'initializes a new document with a virtual attribute' do
|
@@ -30,7 +30,7 @@ describe "Dynamoid::Document" do
|
|
30
30
|
|
31
31
|
@address.new_record.should be_true
|
32
32
|
|
33
|
-
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>"Chicago", :options=>nil}
|
33
|
+
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>"Chicago", :options=>nil, :deliverable => nil}
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'allows interception of write_attribute on load' do
|
@@ -36,13 +36,13 @@ describe "Dynamoid::Fields" do
|
|
36
36
|
@address.updated_at.should_not be_nil
|
37
37
|
@address.updated_at.class.should == DateTime
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
context 'with a saved address' do
|
41
41
|
before do
|
42
|
-
@address = Address.create
|
42
|
+
@address = Address.create(:deliverable => true)
|
43
43
|
@original_id = @address.id
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
it 'should write an attribute correctly' do
|
47
47
|
@address.write_attribute(:city, 'Chicago')
|
48
48
|
end
|
@@ -85,7 +85,7 @@ describe "Dynamoid::Fields" do
|
|
85
85
|
end
|
86
86
|
|
87
87
|
it 'returns all attributes' do
|
88
|
-
Address.attributes.should == {:id=>{:type=>:string}, :created_at=>{:type=>:datetime}, :updated_at=>{:type=>:datetime}, :city=>{:type=>:string}, :options=>{:type=>:serialized}}
|
88
|
+
Address.attributes.should == {:id=>{:type=>:string}, :created_at=>{:type=>:datetime}, :updated_at=>{:type=>:datetime}, :city=>{:type=>:string}, :options=>{:type=>:serialized}, :deliverable => {:type => :boolean}}
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
@@ -89,6 +89,20 @@ describe "Dynamoid::Persistence" do
|
|
89
89
|
@address.options = (hash = {:x => [1, 2], "foobar" => 3.14})
|
90
90
|
Address.undump(@address.send(:dump))[:options].should == hash
|
91
91
|
end
|
92
|
+
|
93
|
+
it 'dumps a boolean field' do
|
94
|
+
@address.deliverable = true
|
95
|
+
Address.undump(@address.send(:dump))[:deliverable].should == true
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'raises on an invalid boolean value' do
|
99
|
+
expect do
|
100
|
+
@address.deliverable = true
|
101
|
+
data = @address.send(:dump)
|
102
|
+
data[:deliverable] = 'foo'
|
103
|
+
Address.undump(data)
|
104
|
+
end.to raise_error(ArgumentError)
|
105
|
+
end
|
92
106
|
|
93
107
|
it 'loads a hash into a serialized field' do
|
94
108
|
hash = {foo: :bar}
|
@@ -126,6 +140,38 @@ describe "Dynamoid::Persistence" do
|
|
126
140
|
|
127
141
|
lambda {Address.create(hash)}.should_not raise_error
|
128
142
|
end
|
143
|
+
|
144
|
+
context 'create' do
|
145
|
+
{
|
146
|
+
Tweet => ['with range', { :tweet_id => 1, :group => 'abc' }],
|
147
|
+
Message => ['without range', { :message_id => 1, :text => 'foo', :time => DateTime.now }]
|
148
|
+
}.each_pair do |clazz, fields|
|
149
|
+
it "checks for existence of an existing object #{fields[0]}" do
|
150
|
+
t1 = clazz.new(fields[1])
|
151
|
+
t2 = clazz.new(fields[1])
|
152
|
+
|
153
|
+
t1.save
|
154
|
+
expect do
|
155
|
+
t2.save!
|
156
|
+
end.to raise_exception AWS::DynamoDB::Errors::ConditionalCheckFailedException
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'raises when dumping a column with an unknown field type' do
|
162
|
+
clazz = Class.new do
|
163
|
+
include Dynamoid::Document
|
164
|
+
table :name => :addresses
|
165
|
+
|
166
|
+
field :city
|
167
|
+
field :options, :serialized
|
168
|
+
field :deliverable, :bad_type_specifier
|
169
|
+
end
|
170
|
+
|
171
|
+
expect do
|
172
|
+
clazz.new(:deliverable => true).dump
|
173
|
+
end.to raise_error(ArgumentError)
|
174
|
+
end
|
129
175
|
|
130
176
|
context 'update' do
|
131
177
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-12-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activemodel
|
@@ -362,7 +362,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
362
362
|
version: '0'
|
363
363
|
segments:
|
364
364
|
- 0
|
365
|
-
hash:
|
365
|
+
hash: 1769474617671935621
|
366
366
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
367
367
|
none: false
|
368
368
|
requirements:
|
@@ -371,7 +371,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
371
371
|
version: '0'
|
372
372
|
requirements: []
|
373
373
|
rubyforge_project:
|
374
|
-
rubygems_version: 1.8.
|
374
|
+
rubygems_version: 1.8.24
|
375
375
|
signing_key:
|
376
376
|
specification_version: 3
|
377
377
|
summary: Dynamoid is an ORM for Amazon's DynamoDB
|