dynamoid 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a2fdea2ccbdac2b53135bbd2bed6dedc5b501551
4
- data.tar.gz: 31174f8f8b12baf0b7f40ab573dc7eed0f05d057
3
+ metadata.gz: c5d8f6269824b26eaae72aa50582f732cf12d32a
4
+ data.tar.gz: b2d435682f178120634e816294f9903c205c2a41
5
5
  SHA512:
6
- metadata.gz: 1de8d6315cbef5ce26f26a850cb95669e93a4d4b3e4badc2ea5305e224dcdf4ffcaf66cf2d0cd5baca4e7644b57765bb92cf31f841ed1fed1cc1d6a789433d7a
7
- data.tar.gz: 33f582e685f63655d6b57c9cb5484eacf71d04634e34829370aff67fc173511c4f26acba4affb151f0774998477577ed8dd0a66f8af8615e41d12797163212ac
6
+ metadata.gz: 8cda5990c7916d53b5a34fb1c6b66cba13191f51627bd100fb276354e391c73168a7c06b01c27da946b3b164e12b15727ef1f41aa1ebd60b4e17128ce6ab2e4c
7
+ data.tar.gz: 821224d319424d6bb75e76b8ba17ed9832a73d9efab46622b6666ab0239c46515b883a37797e3fbec527fec98b0cd09929e5804e81d926d633d4c41bacc4c9ba
@@ -12,6 +12,38 @@
12
12
 
13
13
  * N/A
14
14
 
15
+ # 2.1.0
16
+
17
+ ## Breaking
18
+
19
+ * N/A
20
+
21
+ ## Improvements
22
+
23
+ * Feature: [#221](https://github.com/Dynamoid/Dynamoid/pull/221) Add field declaration option `of` to specify the type of `set` elements (@pratik60)
24
+ * Feature: [#223](https://github.com/Dynamoid/Dynamoid/pull/223) Add field declaration option `store_as_string` to store `datetime` as ISO-8601 formatted strings (@william101)
25
+ * Feature: [#228](https://github.com/Dynamoid/Dynamoid/pull/228) Add field declaration option `store_as_string` to store `date` as ISO-8601 formatted strings (@andrykonchin)
26
+ * Feature: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Support hash argument for `start` chain method (@mnussbaumer)
27
+ * Feature: [#236](https://github.com/Dynamoid/Dynamoid/pull/236) Change log level from `info` to `debug` for benchmark logging (@kicktheken)
28
+ * Feature: [#239](https://github.com/Dynamoid/Dynamoid/pull/239) Add methods for low-level updating: `.update`, `.update_fields` and `.upsert` (@andrykonchin)
29
+ * Feature: [#243](https://github.com/Dynamoid/Dynamoid/pull/243) Support `ne` condition operator (@andrykonchin)
30
+ * Feature: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Added support of backoff in batch operations (@andrykonchin)
31
+ * added global config options `backoff` and `backoff_strategies` to configure backoff
32
+ * added `constant` and `exponential` built-in backoff strategies
33
+ * `.find_all` and `.import` support new backoff options
34
+
35
+ ## Fixes
36
+
37
+ * Bug: [#216](https://github.com/Dynamoid/Dynamoid/pull/216) Fix global index detection in queries with conditions other than equal (@andrykonchin)
38
+ * Bug: [#224](https://github.com/Dynamoid/Dynamoid/pull/224) Fix how `contains` operator works with `set` and `array` field types (@andrykonchin)
39
+ * Bug: [#225](https://github.com/Dynamoid/Dynamoid/pull/225) Fix equal conditions for `array` fields (@andrykonchin)
40
+ * Bug: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Repair support `start` chain method on Scan operation (@mnussbaumer)
41
+ * Bug: [#238](https://github.com/Dynamoid/Dynamoid/pull/238) Fix default value of `models_dir` config option (@baloran)
42
+ * Bug: [#244](https://github.com/Dynamoid/Dynamoid/pull/244) Allow to pass empty strings and sets to `.import` (@andrykonchin)
43
+ * Bug: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Batch operations (`batch_write_item` and `batch_read_item`) handle unprocessed items themselves (@andrykonchin)
44
+ * Bug: [#250](https://github.com/Dynamoid/Dynamoid/pull/250) Update outdated warning message about inefficient query and missing indices (@andrykonchin)
45
+ * Bug: [252](https://github.com/Dynamoid/Dynamoid/pull/252) Don't loose nanoseconds when store DateTime as float number
46
+
15
47
  # 2.0.0
16
48
 
17
49
  ## Breaking
@@ -1,20 +1,22 @@
1
+ MIT License
2
+
1
3
  Copyright (c) 2012 Josh Symonds
4
+ Copyright (c) 2013 - 2018 Dynamoid, https://github.com/Dynamoid
2
5
 
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
10
12
 
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
13
15
 
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Dynamoid
2
2
 
3
- You are viewing the README for the unreleased version 2 of Dynamoid.
4
-
3
+ You are viewing the README for version 2 of Dynamoid. See the [CHANGELOG](https://github.com/Dynamoid/Dynamoid/blob/master/CHANGELOG.md#200) for details on breaking changes since 1.3.x.
4
+
5
5
  For version 1.3.x use the [1-3-stable branch](https://github.com/Dynamoid/Dynamoid/blob/1-3-stable/README.md).
6
6
 
7
7
  Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
@@ -19,13 +19,13 @@ But if you want a fast, scalable, simple, easy-to-use database (and a Gem that s
19
19
  |------------------------ | ----------------- |
20
20
  | gem name | dynamoid |
21
21
  | license | MIT |
22
- | download rank | [![Total Downloads](https://img.shields.io/gem/rt/Dynamoid.png)](https://rubygems.org/gems/dynamoid) |
23
- | version | [![Gem Version](https://badge.fury.io/rb/dynamoid.png)](http://badge.fury.io/rb/dynamoid) |
24
- | dependencies | [![Dependency Status](https://gemnasium.com/badges/github.com/Dynamoid/Dynamoid.png)](https://gemnasium.com/github.com/Dynamoid/Dynamoid) |
25
- | code quality | [![Code Climate](https://codeclimate.com/github/Dynamoid/Dynamoid.png)](https://codeclimate.com/github/Dynamoid/Dynamoid) |
26
- | continuous integration | [![Build Status](https://secure.travis-ci.org/Dynamoid/Dynamoid.png?branch=master)](https://travis-ci.org/Dynamoid/Dynamoid) |
27
- | test coverage | [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/Dynamoid/badge.png?branch=master)](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
28
- | triage helpers | [![Coverage Status](https://www.codetriage.com/dynamoid/dynamoid/badges/users.png)](https://www.codetriage.com/dynamoid/dynamoid) |
22
+ | download rank | [![Total Downloads](https://img.shields.io/gem/rt/Dynamoid.svg)](https://rubygems.org/gems/dynamoid) |
23
+ | version | [![Gem Version](https://badge.fury.io/rb/dynamoid.svg)](https://rubygems.org/gems/dynamoid) |
24
+ | dependencies | [![Dependency Status](https://gemnasium.com/badges/github.com/Dynamoid/Dynamoid.svg)](https://gemnasium.com/github.com/Dynamoid/Dynamoid) [![Depfu](https://badges.depfu.com/badges/6661c063c8e77a5008344fc7283a50aa/status.svg)](https://depfu.com)|
25
+ | code quality | [![Code Climate](https://codeclimate.com/github/Dynamoid/Dynamoid.svg)](https://codeclimate.com/github/Dynamoid/Dynamoid) |
26
+ | continuous integration | [![Build Status](https://travis-ci.org/Dynamoid/Dynamoid.svg?branch=master)](https://travis-ci.org/Dynamoid/Dynamoid) |
27
+ | test coverage | [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/Dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
28
+ | triage helpers | [![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid) |
29
29
  | homepage | [https://github.com/Dynamoid/Dynamoid](https://github.com/Dynamoid/Dynamoid) |
30
30
  | documentation | [http://rdoc.info/github/Dynamoid/Dynamoid/frames](http://rdoc.info/github/Dynamoid/Dynamoid/frames) |
31
31
 
@@ -34,11 +34,11 @@ But if you want a fast, scalable, simple, easy-to-use database (and a Gem that s
34
34
  Installing Dynamoid is pretty simple. First include the Gem in your Gemfile:
35
35
 
36
36
  ```ruby
37
- gem 'dynamoid', '~> 1'
37
+ gem 'dynamoid', '~> 2'
38
38
  ```
39
39
  ## Prerequisities
40
40
 
41
- Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (~> 4).
41
+ Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (>= 4).
42
42
  Hence the configuration as needed for aws to work will be dealt with by aws setup.
43
43
 
44
44
  Here are the steps to setup aws-sdk.
@@ -49,7 +49,18 @@ gem 'aws-sdk', '~>2'
49
49
 
50
50
  (or) include the aws-sdk in your Gemfile.
51
51
 
52
- **NOTE:** Dynamoid-1.0 doesn't support aws-sdk Version 1 (Use Dynamoid Major Version 0 for aws-sdk 1)
52
+ ### AWS SDK Version Compatibility
53
+
54
+ Make sure you are using the version for the right AWS SDK.
55
+
56
+ | Dynamoid version | AWS SDK Version |
57
+ | ---------------- | --------------- |
58
+ | 0.x | 1.x |
59
+ | 1.x | 2.x |
60
+ | 2.x | 2.x |
61
+ | 3.x (unreleased) | 3.x |
62
+
63
+ ### AWS Configuration
53
64
 
54
65
  Configure AWS access:
55
66
  [Reference](https://github.com/aws/aws-sdk-ruby)
@@ -70,6 +81,7 @@ Create config/initializers/aws.rb as follows:
70
81
  Alternatively, if you don't want Aws connection settings to be overwritten for you entire project, you can specify connection settings for Dynamoid only, by setting those in the `Dynamoid.configure` clause:
71
82
 
72
83
  ```ruby
84
+ require 'dynamoid'
73
85
  Dynamoid.configure do |config|
74
86
  config.access_key = 'REPLACE_WITH_ACCESS_KEY_ID'
75
87
  config.secret_key = 'REPLACE_WITH_SECRET_ACCESS_KEY'
@@ -83,6 +95,7 @@ For a full list of the DDB regions, you can go
83
95
  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):
84
96
 
85
97
  ```ruby
98
+ require 'dynamoid'
86
99
  Dynamoid.configure do |config|
87
100
  config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have. Set to nil to avoid namespacing.
88
101
  config.endpoint = 'http://localhost:3000' # [Optional]. If provided, it communicates with the DB listening at the endpoint. This is useful for testing with [Amazon Local DB] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html).
@@ -90,7 +103,7 @@ Then you need to initialize Dynamoid config to get it going. Put code similar to
90
103
 
91
104
  ```
92
105
 
93
- ### Compatibility Matrix
106
+ ### Ruby & Rails Compatibility Matrix
94
107
 
95
108
  | Ruby / Active Record | 4.0.x | 4.1.x | 4.2.x | 5.0.x |
96
109
  |:---------------------:|:-----:|:-----:|:-----:|:-----:|
@@ -153,6 +166,44 @@ class Document
153
166
  end
154
167
  ```
155
168
 
169
+ #### Note on date type
170
+
171
+ By default date fields are persisted as days count since 1 January 1970 like UNIX time. If you prefer dates to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true`
172
+
173
+ ```ruby
174
+ class Document
175
+ include DynamoId::Document
176
+
177
+ field :sent_at, :datetime, store_as_string: true
178
+ end
179
+ ```
180
+
181
+ #### Note on datetime type
182
+
183
+ By default datetime fields are persisted as UNIX timestamps with milisecond precission in DynamoDB. If you prefer datetimes to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true`
184
+
185
+ ```ruby
186
+ class Document
187
+ include DynamoId::Document
188
+
189
+ field :sent_at, :datetime, store_as_string: true
190
+ end
191
+ ```
192
+
193
+ ### Note on set type
194
+
195
+ There is `of` option to declare the type of set elements. You can use
196
+ `:integer` value only
197
+
198
+ ```ruby
199
+ class Document
200
+ include DynamoId::Document
201
+
202
+ field :tags, :set, of: :integer
203
+ end
204
+ ```
205
+
206
+
156
207
  #### Magic Columns
157
208
 
158
209
  You get magic columns of id (string), created_at (datetime), and updated_at (datetime) for free.
@@ -403,11 +454,13 @@ There are three types of limits that you can query with:
403
454
  Using these in various combinations results in the underlying requests to be made in the smallest size possible and
404
455
  the query returns once `record_limit` or `scan_limit` is satisfied. It will attempt to batch whenever possible.
405
456
 
406
- You can thus limit the number of evaluated records, or select a record from which to start, to support pagination:
457
+ You can thus limit the number of evaluated records, or select a record from which to start in order to support pagination.
407
458
 
408
459
  ```ruby
409
460
  Address.record_limit(5).start(address) # Only 5 addresses starting at `address`
410
461
  ```
462
+ Where `address` is an instance of the model or a hash `{the_model_hash_key: 'value', the_model_range_key: 'value'}`:
463
+ Keep in mind that if you are passing a hash to `.start()` you need to explicitly define all required keys in it including range keys, depending on table or secondary indexes signatures, otherwise you'll get an `Aws::DynamoDB::Errors::ValidationException` either for `Exclusive Start Key must have same size as table's key schema` or `The provided starting key is invalid`
411
464
 
412
465
  If you are potentially running over a large data set and this is especially true when using certain filters, you may
413
466
  want to consider limiting the number of scanned records (the number of records DynamoDB infrastructure looks through
@@ -472,6 +525,41 @@ User.where("created_at.lt" => DateTime.now - 1.day).all
472
525
 
473
526
  It also supports .gte and .lte. Turning those into symbols and allowing a Rails SQL-style string syntax is in the works. You can only have one range argument per query, because of DynamoDB's inherent limitations, so use it sensibly!
474
527
 
528
+
529
+ ### Updating
530
+
531
+ In order to update document you can use high level methods
532
+ `#update_attributes`, `#update_attribute` and `.update`.
533
+ They run validation and collbacks.
534
+
535
+ ```ruby
536
+ Address.find(id).update_attributes(city: 'Chicago')
537
+ Address.find(id).update_attribute(city, 'Chicago')
538
+ Address.update(id, city: 'Chicago')
539
+ Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
540
+ ```
541
+
542
+ There are also some low level methods `#update`, `.update_fields` and
543
+ `.upsert`. They don't run validation and callbacks (except `#update` - it
544
+ runs `update` callbacks). All of them support conditional updates.
545
+ `#upsert` will create new document if document with specified `id`
546
+ doesn't exist.
547
+
548
+ ```ruby
549
+ Adderess.find(id).update do |i|
550
+ i.set city: 'Chicago'
551
+ i.add latitude: 100
552
+ i.delete set_of_numbers: 10
553
+ end
554
+ Adderess.find(id).update(if: { deliverable: true }) do |i|
555
+ i.set city: 'Chicago'
556
+ end
557
+ Address.update_fields(id, city: 'Chicago')
558
+ Address.update_fields(id, { city: 'Chicago' }, if: { deliverable: true })
559
+ Address.upsert(id, city: 'Chicago')
560
+ Address.upsert(id, { city: 'Chicago' }, if: { deliverable: true })
561
+ ```
562
+
475
563
  ### Deleting
476
564
 
477
565
  In order to delete some items `delete_all` method should be used.
@@ -587,9 +675,13 @@ Listed below are all configuration options.
587
675
  * `sync_retry_max_times` - when Dynamoid creates or deletes table synchronously it checks for completion specified times. Default is 60 (times). It's a bit over 2 minutes by default
588
676
  * `sync_retry_wait_seconds` - time to wait between retries. Default is 2 (seconds)
589
677
  * `convert_big_decimal` - if `true` then Dynamoid converts numbers stored in `Hash` in `raw` field to float. Default is `false`
590
- * `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is `app/models`. In Rails application you should set `./app/models` value
678
+ * `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is `./app/models`.
591
679
  * `application_timezone` - Dynamoid converts all `datetime` fields to specified time zone when loads data from the storage.
592
680
  Acceptable values - `utc`, `local` (to use system time zone) and time zone name e.g. `Eastern Time (US & Canada)`. Default is `local`
681
+ * `store_datetime_as_string` - if `true` then Dynamoid stores :datetime fields in ISO 8601 string format. Default is `false`
682
+ * `store_date_as_string` - if `true` then Dynamoid stores :date fields in ISO 8601 string format. Default is `false`
683
+ * `backoff` - is a hash: key is a backoff strategy (symbol), value is parameters for the strategy. Is used in batch operations. Default id `nil`
684
+ * `backoff_strategies`: is a hash and contains all available strategies. Default is { constant: ..., exponential: ...}
593
685
 
594
686
 
595
687
  ## Concurrency
@@ -610,6 +702,52 @@ In this example, all saves to `MyTable` will raise an `Dynamoid::Errors::StaleOb
610
702
 
611
703
  Calls to `update` and `update!` also increment the `lock_version`, however they do not check the existing value. This guarantees that a update operation will raise an exception in a concurrent save operation, however a save operation will never cause an update to fail. Thus, `update` is useful & safe only for doing atomic operations (e.g. increment a value, add/remove from a set, etc), but should not be used in a read-modify-write pattern.
612
704
 
705
+
706
+ ### Backoff strategies
707
+
708
+
709
+ You can use several methods that run efficiently in batch mode like `.find_all` and `.import`.
710
+
711
+ The backoff strategy will be used when, for any reason, some items could not be processed as part of a batch mode command.
712
+ Operations will be re-run to process these items.
713
+
714
+ Exponential backoff is the recommended way to handle throughput limits exceeding and throttling on the table.
715
+
716
+ There are two built-in strategies - constant delay and truncated binary exponential backoff.
717
+ By default no backoff is used but you can specify one of the built-in ones:
718
+
719
+ ```ruby
720
+ Dynamoid.configure do |config|
721
+ config.backoff = { constant: 2.second }
722
+ end
723
+
724
+ Dynamoid.configure do |config|
725
+ config.backoff = { exponential: { base_backoff: 0.2.seconds, ceiling: 10 } }
726
+ end
727
+
728
+ ```
729
+
730
+ You can just specify strategy without any arguments to use default presets:
731
+
732
+ ```ruby
733
+ Dynamoid.configure do |config|
734
+ config.backoff = :constant
735
+ end
736
+ ```
737
+
738
+ You can use your own strategy in following way:
739
+
740
+ ```ruby
741
+ Dynamoid.configure do |config|
742
+ config.backoff_strategies[:custom] = lambda do |n|
743
+ -> { sleep rand(n) }
744
+ end
745
+
746
+ config.backoff = { custom: 10 }
747
+ end
748
+ ```
749
+
750
+
613
751
  ## Rake Tasks
614
752
 
615
753
  * `rake dynamoid:create_tables`
@@ -676,6 +814,7 @@ Also, without contributors the project wouldn't be nearly as awesome. So many th
676
814
  * [Pascal Corpet](https://github.com/pcorpet)
677
815
  * [Brian Glusman](https://github.com/bglusman) *
678
816
  * [Peter Boling](https://github.com/pboling) *
817
+ * [Andrew Konchin](https://github.com/andrykonchin) *
679
818
 
680
819
  \* Current Maintainers
681
820
 
@@ -21,7 +21,8 @@ Gem::Specification.new do |spec|
21
21
  "Sumanth Ravipati",
22
22
  "Pascal Corpet",
23
23
  "Brian Glusman",
24
- "Peter Boling"
24
+ "Peter Boling",
25
+ "Andrew Konchin"
25
26
  ]
26
27
  spec.email = ["peter.boling@gmail.com", "brian@stellaservice.com"]
27
28
 
@@ -51,7 +51,7 @@ module Dynamoid
51
51
  def benchmark(method, *args)
52
52
  start = Time.now
53
53
  result = yield
54
- Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
54
+ Dynamoid.logger.debug "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
55
55
  return result
56
56
  end
57
57
 
@@ -80,12 +80,12 @@ module Dynamoid
80
80
  # unless multiple ids are passed in.
81
81
  #
82
82
  # @since 0.2.0
83
- def read(table, ids, options = {})
83
+ def read(table, ids, options = {}, &blk)
84
84
  range_key = options.delete(:range_key)
85
85
 
86
86
  if ids.respond_to?(:each)
87
87
  ids = ids.collect{|id| range_key ? [id, range_key] : id}
88
- batch_get_item({table => ids}, options)
88
+ batch_get_item({table => ids}, options, &blk)
89
89
  else
90
90
  options[:range_key] = range_key if range_key
91
91
  get_item(table, ids, options)
@@ -144,8 +144,12 @@ module Dynamoid
144
144
  # Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
145
145
  #
146
146
  # @since 0.2.0
147
- define_method(m) do |*args|
148
- benchmark("#{m.to_s}", *args) {adapter.send(m, *args)}
147
+ define_method(m) do |*args, &blk|
148
+ if blk.present?
149
+ benchmark("#{m.to_s}", *args) { adapter.send(m, *args, &blk) }
150
+ else
151
+ benchmark("#{m.to_s}", *args) { adapter.send(m, *args) }
152
+ end
149
153
  end
150
154
  end
151
155
 
@@ -18,6 +18,7 @@ module Dynamoid
18
18
  # we declare schema in models
19
19
  FIELD_MAP = {
20
20
  eq: 'EQ',
21
+ ne: 'NE',
21
22
  gt: 'GT',
22
23
  lt: 'LT',
23
24
  gte: 'GE',
@@ -83,36 +84,54 @@ module Dynamoid
83
84
  @client
84
85
  end
85
86
 
86
- # Puts or deletes multiple items in one or more tables
87
+ # Puts multiple items in one table
88
+ #
89
+ # If optional block is passed it will be called for each written batch of items, meaning once per batch.
90
+ # Block receives boolean flag which is true if there are some unprocessed items, otherwise false.
91
+ #
92
+ # @example Saves several items to the table testtable
93
+ # Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', [{ id: '1', name: 'a' }, { id: '2', name: 'b'}])
94
+ #
95
+ # @example Pass block
96
+ # Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', items) do |bool|
97
+ # if bool
98
+ # puts 'there are unprocessed items'
99
+ # end
100
+ # end
87
101
  #
88
102
  # @param [String] table_name the name of the table
89
103
  # @param [Array] items to be processed
90
104
  # @param [Hash] additional options
105
+ # @param [Proc] optional block
91
106
  #
92
107
  # See:
93
108
  # * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
94
109
  # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
95
- #
96
- # TODO handle rejections because of exceeding limit for the whole request - 16 MB,
97
- # item size limit - 400 KB or because provisioned throughput is exceeded
98
110
  def batch_write_item table_name, objects, options = {}
99
- requests = []
100
-
101
- objects.each_slice(BATCH_WRITE_ITEM_REQUESTS_LIMIT) do |os|
102
- requests << os.map { |o| { put_request: { item: o } } }
103
- end
111
+ items = objects.map { |o| sanitize_item(o) }
104
112
 
105
113
  begin
106
- requests.each do |request_items|
107
- client.batch_write_item(
114
+ while items.present? do
115
+ batch = items.shift(BATCH_WRITE_ITEM_REQUESTS_LIMIT)
116
+ requests = batch.map { |item| { put_request: { item: item } } }
117
+
118
+ response = client.batch_write_item(
108
119
  {
109
120
  request_items: {
110
- table_name => request_items,
121
+ table_name => requests,
111
122
  },
112
123
  return_consumed_capacity: 'TOTAL',
113
124
  return_item_collection_metrics: 'SIZE'
114
125
  }.merge!(options)
115
126
  )
127
+
128
+ if block_given?
129
+ yield(response.unprocessed_items.present?)
130
+ end
131
+
132
+ if response.unprocessed_items.present?
133
+ items += response.unprocessed_items[table_name].map { |r| r.put_request.item }
134
+ end
116
135
  end
117
136
  rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
118
137
  raise Dynamoid::Errors::ConditionalCheckFailedException, e
@@ -121,17 +140,37 @@ module Dynamoid
121
140
 
122
141
  # Get many items at once from DynamoDB. More efficient than getting each item individually.
123
142
  #
143
+ # If optional block is passed `nil` will be returned and the block will be called for each read batch of items,
144
+ # meaning once per batch.
145
+ #
146
+ # Block receives parameters:
147
+ # * hash with items like `{ table_name: [items]}`
148
+ # * and boolean flag is true if there are some unprocessed keys, otherwise false.
149
+ #
124
150
  # @example Retrieve IDs 1 and 2 from the table testtable
125
- # Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item({'table1' => ['1', '2']})
151
+ # Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ['1', '2'])
152
+ #
153
+ # @example Pass block to receive each batch
154
+ # Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ids) do |hash, bool|
155
+ # puts hash['table1']
156
+ #
157
+ # if bool
158
+ # puts 'there are unprocessed keys'
159
+ # end
160
+ # end
126
161
  #
127
162
  # @param [Hash] table_ids the hash of tables and IDs to retrieve
128
163
  # @param [Hash] options to be passed to underlying BatchGet call
164
+ # @param [Proc] optional block can be passed to handle each batch of items
129
165
  #
130
166
  # @return [Hash] a hash where keys are the table names and the values are the retrieved items
131
167
  #
168
+ # See:
169
+ # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
170
+ #
132
171
  # @since 1.0.0
133
172
  #
134
- # @todo: Provide support for passing options to underlying batch_get_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
173
+ # @todo: Provide support for passing options to underlying batch_get_item
135
174
  def batch_get_item(table_ids, options = {})
136
175
  request_items = Hash.new{|h, k| h[k] = []}
137
176
  return request_items if table_ids.all?{|k, v| v.blank?}
@@ -140,19 +179,22 @@ module Dynamoid
140
179
 
141
180
  table_ids.each do |t, ids|
142
181
  next if ids.blank?
182
+ ids = Array(ids).dup
143
183
  tbl = describe_table(t)
144
184
  hk = tbl.hash_key.to_s
145
185
  rng = tbl.range_key.to_s
146
186
 
147
- Array(ids).each_slice(Dynamoid::Config.batch_size) do |ids|
187
+ while ids.present? do
188
+ batch = ids.shift(Dynamoid::Config.batch_size)
189
+
148
190
  request_items = Hash.new{|h, k| h[k] = []}
149
191
 
150
192
  keys = if rng.present?
151
- Array(ids).map do |h, r|
193
+ Array(batch).map do |h, r|
152
194
  { hk => h, rng => r }
153
195
  end
154
196
  else
155
- Array(ids).map do |id|
197
+ Array(batch).map do |id|
156
198
  { hk => id }
157
199
  end
158
200
  end
@@ -165,13 +207,29 @@ module Dynamoid
165
207
  request_items: request_items
166
208
  )
167
209
 
168
- results.data[:responses].each do |table, rows|
169
- ret[table] += rows.collect { |r| result_item_to_hash(r) }
210
+ unless block_given?
211
+ results.data[:responses].each do |table, rows|
212
+ ret[table] += rows.collect { |r| result_item_to_hash(r) }
213
+ end
214
+ else
215
+ batch_results = Hash.new([].freeze)
216
+
217
+ results.data[:responses].each do |table, rows|
218
+ batch_results[table] += rows.collect { |r| result_item_to_hash(r) }
219
+ end
220
+
221
+ yield(batch_results, results.unprocessed_keys.present?)
222
+ end
223
+
224
+ if results.unprocessed_keys.present?
225
+ ids += results.unprocessed_keys[t].keys.map { |h| h[hk] }
170
226
  end
171
227
  end
172
228
  end
173
229
 
174
- ret
230
+ unless block_given?
231
+ ret
232
+ end
175
233
  end
176
234
 
177
235
  # Delete many items at once from DynamoDB. More efficient than delete each item individually.
@@ -421,13 +479,8 @@ module Dynamoid
421
479
  #
422
480
  # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
423
481
  def put_item(table_name, object, options = {})
424
- item = {}
425
482
  options ||= {}
426
-
427
- object.each do |k, v|
428
- next if v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
429
- item[k.to_s] = v
430
- end
483
+ item = sanitize_item(object)
431
484
 
432
485
  begin
433
486
  client.put_item(
@@ -480,39 +533,21 @@ module Dynamoid
480
533
  record_limit = opts.delete(:record_limit)
481
534
  scan_limit = opts.delete(:scan_limit)
482
535
  batch_size = opts.delete(:batch_size)
483
- limit = [record_limit, scan_limit, batch_size].compact.min
484
- q[:limit] = limit if limit
485
-
486
- opts.delete(:next_token).tap do |token|
487
- break unless token
488
- q[:exclusive_start_key] = {
489
- hk => token[:hash_key_element],
490
- rng => token[:range_key_element]
491
- }
492
- # For secondary indices the start key must contain the indices composite key
493
- # but also the table's composite keys
494
- q[:exclusive_start_key][table.hash_key] = token[:table_hash_key_element] if token[:table_hash_key_element]
495
- q[:exclusive_start_key][table.range_key] = token[:table_range_key_element] if token[:table_range_key_element]
496
- end
536
+ exclusive_start_key = opts.delete(:exclusive_start_key)
537
+ limit = [record_limit, scan_limit, batch_size].compact.min
497
538
 
498
539
  key_conditions = {
499
540
  hk => {
500
- # TODO: Provide option for other operators like NE, IN, LE, etc
501
541
  comparison_operator: EQ,
502
- attribute_value_list: [
503
- opts.delete(:hash_value).freeze
504
- ]
542
+ attribute_value_list: attribute_value_list(EQ, opts.delete(:hash_value).freeze)
505
543
  }
506
544
  }
507
545
 
508
546
  opts.each_pair do |k, v|
509
- # TODO: ATM, only few comparison operators are supported, provide support for all operators
510
547
  next unless(op = RANGE_MAP[k])
511
548
  key_conditions[rng] = {
512
549
  comparison_operator: op,
513
- attribute_value_list: [
514
- opts.delete(k).freeze
515
- ].flatten # Flatten as BETWEEN operator specifies array of two elements
550
+ attribute_value_list: attribute_value_list(op, opts.delete(k).freeze)
516
551
  }
517
552
  end
518
553
 
@@ -520,12 +555,12 @@ module Dynamoid
520
555
  opts.reject {|k, _| k.in? RANGE_MAP.keys}.each do |attr, hash|
521
556
  query_filter[attr] = {
522
557
  comparison_operator: FIELD_MAP[hash.keys[0]],
523
- attribute_value_list: [
524
- hash.values[0].freeze
525
- ].flatten # Flatten as BETWEEN operator specifies array of two elements
558
+ attribute_value_list: attribute_value_list(FIELD_MAP[hash.keys[0]], hash.values[0].freeze)
526
559
  }
527
560
  end
528
561
 
562
+ q[:limit] = limit if limit
563
+ q[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
529
564
  q[:table_name] = table_name
530
565
  q[:key_conditions] = key_conditions
531
566
  q[:query_filter] = query_filter
@@ -593,15 +628,16 @@ module Dynamoid
593
628
  record_limit = select_opts.delete(:record_limit)
594
629
  scan_limit = select_opts.delete(:scan_limit)
595
630
  batch_size = select_opts.delete(:batch_size)
631
+ exclusive_start_key = select_opts.delete(:exclusive_start_key)
596
632
  request_limit = [record_limit, scan_limit, batch_size].compact.min
597
633
  request[:limit] = request_limit if request_limit
598
-
634
+ request[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
635
+
599
636
  if scan_hash.present?
600
637
  request[:scan_filter] = scan_hash.reduce({}) do |memo, (attr, cond)|
601
- # Flatten as BETWEEN operator specifies array of two elements
602
638
  memo.merge(attr.to_s => {
603
639
  comparison_operator: FIELD_MAP[cond.keys[0]],
604
- attribute_value_list: [cond.values[0].freeze].flatten
640
+ attribute_value_list: attribute_value_list(FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
605
641
  })
606
642
  end
607
643
  end
@@ -748,6 +784,10 @@ module Dynamoid
748
784
  conditions.delete(:unless_exists).try(:each) do |col|
749
785
  expected[col.to_s][:exists] = false
750
786
  end
787
+ conditions.delete(:if_exists).try(:each) do |col, val|
788
+ expected[col.to_s][:exists] = true
789
+ expected[col.to_s][:value] = val
790
+ end
751
791
  conditions.delete(:if).try(:each) do |col, val|
752
792
  expected[col.to_s][:value] = val
753
793
  end
@@ -919,6 +959,21 @@ module Dynamoid
919
959
  }
920
960
  end
921
961
 
962
+ # Build an array of values for Condition
963
+ # Is used in ScanFilter and QueryFilter
964
+ # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
965
+ # @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
966
+ # @params [Object] value: scalar value or array/set
967
+ def attribute_value_list(operator, value)
968
+ # For BETWEEN and IN operators we should keep value as is (it should be already an array)
969
+ # For all the other operators we wrap the value with array
970
+ if ["BETWEEN", "IN"].include?(operator)
971
+ [value].flatten
972
+ else
973
+ [value]
974
+ end
975
+ end
976
+
922
977
  #
923
978
  # Represents a table. Exposes data from the "DescribeTable" API call, and also
924
979
  # provides methods for coercing values to the proper types based on the table's schema data
@@ -1035,6 +1090,12 @@ module Dynamoid
1035
1090
  DELETE = 'DELETE'.freeze
1036
1091
  PUT = 'PUT'.freeze
1037
1092
  end
1093
+
1094
+ def sanitize_item(attributes)
1095
+ attributes.reject do |k, v|
1096
+ v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
1097
+ end
1098
+ end
1038
1099
  end
1039
1100
  end
1040
1101
  end
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
2
  require 'uri'
3
3
  require 'dynamoid/config/options'
4
+ require 'dynamoid/config/backoff_strategies/constant_backoff'
5
+ require 'dynamoid/config/backoff_strategies/exponential_backoff'
4
6
 
5
7
  module Dynamoid
6
8
 
@@ -26,8 +28,15 @@ module Dynamoid
26
28
  option :sync_retry_max_times, default: 60 # a bit over 2 minutes
27
29
  option :sync_retry_wait_seconds, default: 2
28
30
  option :convert_big_decimal, default: false
29
- option :models_dir, default: 'app/models' # perhaps you keep your dynamoid models in a different directory?
31
+ option :models_dir, default: './app/models' # perhaps you keep your dynamoid models in a different directory?
30
32
  option :application_timezone, default: :local # available values - :utc, :local, time zone names
33
+ option :store_datetime_as_string, default: false # store Time fields in ISO 8601 string format
34
+ option :store_date_as_string, default: false # store Date fields in ISO 8601 string format
35
+ option :backoff, default: nil # callable object to handle exceeding of table throughput limit
36
+ option :backoff_strategies, default: {
37
+ constant: BackoffStrategies::ConstantBackoff,
38
+ exponential: BackoffStrategies::ExponentialBackoff
39
+ }
31
40
 
32
41
  # The default logger for Dynamoid: either the Rails logger or just stdout.
33
42
  #
@@ -55,5 +64,15 @@ module Dynamoid
55
64
  end
56
65
  end
57
66
 
67
+ def build_backoff
68
+ if backoff.is_a?(Hash)
69
+ name = backoff.keys[0]
70
+ args = backoff.values[0]
71
+
72
+ backoff_strategies[name].call(args)
73
+ else
74
+ backoff_strategies[backoff].call
75
+ end
76
+ end
58
77
  end
59
78
  end
@@ -0,0 +1,11 @@
1
+ module Dynamoid
2
+ module Config
3
+ module BackoffStrategies
4
+ class ConstantBackoff
5
+ def self.call(n = 1)
6
+ -> { sleep n }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module Dynamoid
2
+ module Config
3
+ module BackoffStrategies
4
+ # Truncated binary exponential backoff algorithm
5
+ # See https://en.wikipedia.org/wiki/Exponential_backoff
6
+ class ExponentialBackoff
7
+ def self.call(opts = {})
8
+ opts = { base_backoff: 0.5, ceiling: 3 }.merge(opts)
9
+ base_backoff = opts[:base_backoff]
10
+ ceiling = opts[:ceiling]
11
+
12
+ times = 1
13
+
14
+ lambda do
15
+ power = [times - 1, ceiling - 1].min
16
+ backoff = base_backoff * (2 ** power)
17
+ sleep backoff
18
+
19
+ times += 1
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -161,7 +161,10 @@ module Dynamoid #:nodoc:
161
161
  def records_via_scan
162
162
  if Dynamoid::Config.warn_on_scan
163
163
  Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
164
- Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}]"
164
+ Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.downcase}.rb:"
165
+ Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'"
166
+ Dynamoid.logger.warn "* local_secondary_indexe range_key: 'some-name'"
167
+ Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}"
165
168
  end
166
169
 
167
170
  Enumerator.new do |yielder|
@@ -196,6 +199,8 @@ module Dynamoid #:nodoc:
196
199
  val = type_cast_condition_parameter(name, query[key])
197
200
 
198
201
  hash = case operation
202
+ when 'ne'
203
+ { ne: val }
199
204
  when 'gt'
200
205
  { gt: val }
201
206
  when 'lt'
@@ -258,6 +263,8 @@ module Dynamoid #:nodoc:
258
263
  end
259
264
 
260
265
  def type_cast_condition_parameter(key, value)
266
+ return value if [:array, :set].include?(source.attributes[key.to_sym][:type])
267
+
261
268
  if !value.respond_to?(:to_ary)
262
269
  source.dump_field(value, source.attributes[key.to_sym])
263
270
  else
@@ -294,7 +301,7 @@ module Dynamoid #:nodoc:
294
301
  # But only do so if projects ALL attributes otherwise we won't
295
302
  # get back full data
296
303
  source.global_secondary_indexes.each do |_, gsi|
297
- next unless query_keys.include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
304
+ next unless query.keys.map(&:to_s).include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
298
305
  @hash_key = gsi.hash_key
299
306
  @range_key = gsi.range_key
300
307
  @index_name = gsi.name
@@ -309,21 +316,22 @@ module Dynamoid #:nodoc:
309
316
  # If using a secondary index then we must include the index's composite key
310
317
  # as well as the tables composite key.
311
318
  def start_key
319
+ return @start if @start.is_a?(Hash)
312
320
  hash_key = @hash_key || source.hash_key
313
321
  range_key = @range_key || source.range_key
314
322
 
315
323
  key = {}
316
- key[:hash_key_element] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
317
- key[:range_key_element] = type_cast_condition_parameter(range_key, @start.send(range_key)) if range_key
318
-
319
- # Add table composite keys if differ from secondary index used composite key
324
+ key[hash_key] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
325
+ if range_key
326
+ key[range_key] = type_cast_condition_parameter(range_key, @start.send(range_key))
327
+ end
328
+ # Add table composite keys if they differ from secondary index used composite key
320
329
  if hash_key != source.hash_key
321
- key[:table_hash_key_element] = type_cast_condition_parameter(source.hash_key, @start.hash_key)
330
+ key[source.hash_key] = type_cast_condition_parameter(source.hash_key, @start.hash_key)
322
331
  end
323
332
  if source.range_key && range_key != source.range_key
324
- key[:table_range_key_element] = type_cast_condition_parameter(source.range_key, @start.range_value)
333
+ key[source.range_key] = type_cast_condition_parameter(source.range_key, @start.range_value)
325
334
  end
326
-
327
335
  key
328
336
  end
329
337
 
@@ -334,7 +342,7 @@ module Dynamoid #:nodoc:
334
342
  opts[:record_limit] = @record_limit if @record_limit
335
343
  opts[:scan_limit] = @scan_limit if @scan_limit
336
344
  opts[:batch_size] = @batch_size if @batch_size
337
- opts[:next_token] = start_key if @start
345
+ opts[:exclusive_start_key] = start_key if @start
338
346
  opts[:scan_index_forward] = @scan_index_forward
339
347
  opts
340
348
  end
@@ -357,7 +365,7 @@ module Dynamoid #:nodoc:
357
365
  opts[:record_limit] = @record_limit if @record_limit
358
366
  opts[:scan_limit] = @scan_limit if @scan_limit
359
367
  opts[:batch_size] = @batch_size if @batch_size
360
- opts[:next_token] = start_key if @start
368
+ opts[:exclusive_start_key] = start_key if @start
361
369
  opts[:consistent_read] = true if @consistent_read
362
370
  opts
363
371
  end
@@ -119,6 +119,77 @@ module Dynamoid #:nodoc:
119
119
  end
120
120
  end
121
121
 
122
+ def update(hash_key, range_key_value=nil, attrs)
123
+ if range_key.present?
124
+ range_key_value = dump_field(range_key_value, attributes[self.range_key])
125
+ else
126
+ range_key_value = nil
127
+ end
128
+
129
+ model = find(hash_key, range_key: range_key_value, consistent_read: true)
130
+ model.update_attributes(attrs)
131
+ model
132
+ end
133
+
134
+ def update_fields(hash_key_value, range_key_value=nil, attrs={}, conditions={})
135
+ optional_params = [range_key_value, attrs, conditions].compact
136
+ if optional_params.first.is_a?(Hash)
137
+ range_key_value = nil
138
+ attrs, conditions = optional_params[0 .. 1]
139
+ else
140
+ range_key_value = optional_params.first
141
+ attrs, conditions = optional_params[1 .. 2]
142
+ end
143
+
144
+ options = if range_key
145
+ { range_key: dump_field(range_key_value, attributes[range_key]) }
146
+ else
147
+ {}
148
+ end
149
+
150
+ (conditions[:if_exists] ||= {})[hash_key] = hash_key_value
151
+ options[:conditions] = conditions
152
+
153
+ begin
154
+ new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
155
+ attrs.symbolize_keys.each do |k, v|
156
+ t.set k => dump_field(v, attributes[k])
157
+ end
158
+ end
159
+ new(new_attrs)
160
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
161
+ end
162
+ end
163
+
164
+ def upsert(hash_key_value, range_key_value=nil, attrs={}, conditions={})
165
+ optional_params = [range_key_value, attrs, conditions].compact
166
+ if optional_params.first.is_a?(Hash)
167
+ range_key_value = nil
168
+ attrs, conditions = optional_params[0 .. 1]
169
+ else
170
+ range_key_value = optional_params.first
171
+ attrs, conditions = optional_params[1 .. 2]
172
+ end
173
+
174
+ options = if range_key
175
+ { range_key: dump_field(range_key_value, attributes[range_key]) }
176
+ else
177
+ {}
178
+ end
179
+
180
+ options[:conditions] = conditions
181
+
182
+ begin
183
+ new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
184
+ attrs.symbolize_keys.each do |k, v|
185
+ t.set k => dump_field(v, attributes[k])
186
+ end
187
+ end
188
+ new(new_attrs)
189
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
190
+ end
191
+ end
192
+
122
193
  def deep_subclasses
123
194
  subclasses + subclasses.map(&:deep_subclasses).flatten
124
195
  end
@@ -52,9 +52,11 @@ module Dynamoid
52
52
  end
53
53
  end
54
54
 
55
- # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGet.
55
+ # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGetItem.
56
56
  # Returns empty array if no results found.
57
57
  #
58
+ # Uses backoff specified by `Dynamoid::Config.backoff` config option
59
+ #
58
60
  # @param [Array<ID>] ids
59
61
  # @param [Hash] options: Passed to the underlying query.
60
62
  #
@@ -65,8 +67,26 @@ module Dynamoid
65
67
  # find all the tweets using hash key and range key with consistent read
66
68
  # Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
67
69
  def find_all(ids, options = {})
68
- items = Dynamoid.adapter.read(self.table_name, ids, options)
69
- items ? items[self.table_name].map{|i| from_database(i)} : []
70
+ results = unless Dynamoid.config.backoff
71
+ items = Dynamoid.adapter.read(self.table_name, ids, options)
72
+ items ? items[self.table_name] : []
73
+ else
74
+ items = []
75
+ backoff = nil
76
+ Dynamoid.adapter.read(self.table_name, ids, options) do |hash, has_unprocessed_items|
77
+ items += hash[self.table_name]
78
+
79
+ if has_unprocessed_items
80
+ backoff ||= Dynamoid.config.build_backoff
81
+ backoff.call
82
+ else
83
+ backoff = nil
84
+ end
85
+ end
86
+ items
87
+ end
88
+
89
+ results ? results.map {|i| from_database(i) } : []
70
90
  end
71
91
 
72
92
  # Find one object directly by id.
@@ -117,25 +117,14 @@ module Dynamoid
117
117
  value
118
118
  end
119
119
  when :set
120
- Set.new(value)
120
+ undump_set(options, value)
121
121
  when :datetime
122
- if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
123
- value
124
- else
125
- case Dynamoid::Config.application_timezone
126
- when :utc
127
- ActiveSupport::TimeZone['UTC'].at(value).to_datetime
128
- when :local
129
- Time.at(value).to_datetime
130
- when String
131
- ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
132
- end
133
- end
122
+ parse_datetime(value, options)
134
123
  when :date
135
124
  if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
136
125
  value.to_date
137
126
  else
138
- UNIX_EPOCH_DATE + value.to_i
127
+ parse_date(value, options)
139
128
  end
140
129
  when :boolean
141
130
  if value == 't' || value == true
@@ -152,6 +141,17 @@ module Dynamoid
152
141
  end
153
142
  end
154
143
 
144
+ def undump_set(options, value)
145
+ case options[:of]
146
+ when :integer
147
+ value.map { |v| Integer(v) }.to_set
148
+ when :number
149
+ value.map { |v| BigDecimal.new(v.to_s) }.to_set
150
+ else
151
+ value.is_a?(Set) ? value : Set.new(value)
152
+ end
153
+ end
154
+
155
155
  def dump_field(value, options)
156
156
  if (field_class = options[:type]).is_a?(Class)
157
157
  if value.respond_to?(:dynamoid_dump)
@@ -174,9 +174,9 @@ module Dynamoid
174
174
  when :array
175
175
  !value.nil? ? value : nil
176
176
  when :datetime
177
- !value.nil? ? value.to_time.to_f : nil
177
+ !value.nil? ? format_datetime(value, options) : nil
178
178
  when :date
179
- !value.nil? ? (value.to_date - UNIX_EPOCH_DATE).to_i : nil
179
+ !value.nil? ? format_date(value, options) : nil
180
180
  when :serialized
181
181
  options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
182
182
  when :raw
@@ -212,14 +212,38 @@ module Dynamoid
212
212
  end
213
213
  end
214
214
 
215
+ # Creates several models at once.
216
+ # Neither callbacks nor validations run.
217
+ # It works efficiently because of using BatchWriteItem.
218
+ #
219
+ # Returns array of models
220
+ #
221
+ # Uses backoff specified by `Dynamoid::Config.backoff` config option
222
+ #
223
+ # @param [Array<Hash>] items
224
+ #
225
+ # @example
226
+ # User.import([{ name: 'a' }, { name: 'b' }])
215
227
  def import(objects)
216
- documents = objects.map { |attrs|
217
- self.build(attrs).tap { |item|
228
+ documents = objects.map do |attrs|
229
+ self.build(attrs).tap do |item|
218
230
  item.hash_key = SecureRandom.uuid if item.hash_key.blank?
219
- }
220
- }
231
+ end
232
+ end
221
233
 
222
- Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump))
234
+ unless Dynamoid.config.backoff
235
+ Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump))
236
+ else
237
+ backoff = nil
238
+ Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump)) do |has_unprocessed_items|
239
+ if has_unprocessed_items
240
+ backoff ||= Dynamoid.config.build_backoff
241
+ backoff.call
242
+ else
243
+ backoff = nil
244
+ end
245
+ end
246
+ end
223
247
 
224
248
  documents.each { |d| d.new_record = false }
225
249
  documents
@@ -250,6 +274,63 @@ module Dynamoid
250
274
  end
251
275
  end
252
276
 
277
+ def format_datetime(value, options)
278
+ use_string_format = options[:store_as_string].nil? \
279
+ ? Dynamoid.config.store_datetime_as_string \
280
+ : options[:store_as_string]
281
+
282
+ if use_string_format
283
+ value.to_time.iso8601
284
+ else
285
+ unless value.respond_to?(:to_i) && value.respond_to?(:nsec)
286
+ value = value.to_time
287
+ end
288
+ BigDecimal("%d.%09d" % [value.to_i, value.nsec])
289
+ end
290
+ end
291
+
292
+ def format_date(value, options)
293
+ use_string_format = options[:store_as_string].nil? \
294
+ ? Dynamoid.config.store_date_as_string \
295
+ : options[:store_as_string]
296
+
297
+ unless use_string_format
298
+ (value.to_date - UNIX_EPOCH_DATE).to_i
299
+ else
300
+ value.to_date.iso8601
301
+ end
302
+ end
303
+
304
+ def parse_datetime(value, options)
305
+ return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
306
+
307
+ use_string_format = options[:store_as_string].nil? \
308
+ ? Dynamoid.config.store_datetime_as_string \
309
+ : options[:store_as_string]
310
+ value = DateTime.iso8601(value).to_time.to_i if use_string_format
311
+
312
+ case Dynamoid::Config.application_timezone
313
+ when :utc
314
+ ActiveSupport::TimeZone['UTC'].at(value).to_datetime
315
+ when :local
316
+ Time.at(value).to_datetime
317
+ when String
318
+ ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
319
+ end
320
+ end
321
+
322
+ def parse_date(value, options)
323
+ use_string_format = options[:store_as_string].nil? \
324
+ ? Dynamoid.config.store_date_as_string \
325
+ : options[:store_as_string]
326
+
327
+ unless use_string_format
328
+ UNIX_EPOCH_DATE + value.to_i
329
+ else
330
+ Date.iso8601(value)
331
+ end
332
+ end
333
+
253
334
  # Evaluates the default value given, this is used by undump
254
335
  # when determining the value of the default given for a field options.
255
336
  #
@@ -1,3 +1,3 @@
1
1
  module Dynamoid
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1.0'
3
3
  end
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: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Symonds
@@ -17,10 +17,11 @@ authors:
17
17
  - Pascal Corpet
18
18
  - Brian Glusman
19
19
  - Peter Boling
20
+ - Andrew Konchin
20
21
  autorequire:
21
22
  bindir: exe
22
23
  cert_chain: []
23
- date: 2017-12-20 00:00:00.000000000 Z
24
+ date: 2018-04-21 00:00:00.000000000 Z
24
25
  dependencies:
25
26
  - !ruby/object:Gem::Dependency
26
27
  name: activemodel
@@ -234,6 +235,8 @@ files:
234
235
  - lib/dynamoid/associations/single_association.rb
235
236
  - lib/dynamoid/components.rb
236
237
  - lib/dynamoid/config.rb
238
+ - lib/dynamoid/config/backoff_strategies/constant_backoff.rb
239
+ - lib/dynamoid/config/backoff_strategies/exponential_backoff.rb
237
240
  - lib/dynamoid/config/options.rb
238
241
  - lib/dynamoid/criteria.rb
239
242
  - lib/dynamoid/criteria/chain.rb
@@ -271,7 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
271
274
  version: '0'
272
275
  requirements: []
273
276
  rubyforge_project:
274
- rubygems_version: 2.6.14
277
+ rubygems_version: 2.4.5
275
278
  signing_key:
276
279
  specification_version: 4
277
280
  summary: Dynamoid is an ORM for Amazon's DynamoDB