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 CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "dynamoid"
8
- s.version = "0.5.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-08-22"
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.23"
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
- Then you need to initialize it 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
+ 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
- # config.adapter = 'aws_sdk' # This adapter establishes a connection to the DynamoDB servers using Amazon's own AWS gem.
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. See LICENSE.txt for further details.
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.5.0
1
+ 0.6.0
@@ -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 [String] id the id of the record
95
- # @param [Number] range_key the range key of the record
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
- # @since 0.2.0
98
- def delete(table, id, options = {})
99
- if Dynamoid::Config.partitioning?
100
- id_with_partitions(id).each {|i| delete_item(table, i, options)}
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
- delete_item(table, id, options)
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 one, and return only it. Compares each result by
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
- {}.tap do |hash|
153
- Array(results).each do |result|
154
- next if result.nil?
155
- id = result[:id].split('.').first
156
- if !hash[id] || (result[:updated_at] > hash[id][:updated_at])
157
- result[:id] = id
158
- hash[id] = result
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
- end.values
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
- end
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
- @@connection = AWS::DynamoDB.new(:access_key_id => Dynamoid::Config.access_key, :secret_access_key => Dynamoid::Config.secret_key, :dynamo_db_endpoint => Dynamoid::Config.endpoint, :use_ssl => Dynamoid::Config.use_ssl, :dynamo_db_port => Dynamoid::Config.port)
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(object.delete_if{|k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?)})
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
@@ -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 => '80'
26
+ option :port, :default => '443'
27
27
  option :included_models, :default => []
28
28
  option :identity_map, :default => false
29
29
 
@@ -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, query_opts).collect {|hash| source.from_database(hash) }
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!(range_key(key))
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
- key = { :hash_key_element => { 'S' => @start.hash_key } }
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
@@ -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
@@ -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.
@@ -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
- run_callbacks(:create) { persist }
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
@@ -3,6 +3,7 @@ class Address
3
3
 
4
4
  field :city
5
5
  field :options, :serialized
6
+ field :deliverable, :boolean
6
7
 
7
8
  def zip_code=(zip_code)
8
9
  self.city = "Chicago"
@@ -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.5.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-08-22 00:00:00.000000000 Z
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: 396768872286184895
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.23
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