dynamoid 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.rubocop.yml +18 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +15 -0
- data/README.md +113 -63
- data/Vagrantfile +2 -2
- data/docker-compose.yml +1 -1
- data/gemfiles/rails_4_2.gemfile +1 -1
- data/gemfiles/rails_5_0.gemfile +1 -1
- data/gemfiles/rails_5_1.gemfile +1 -1
- data/gemfiles/rails_5_2.gemfile +1 -1
- data/lib/dynamoid/adapter.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
- data/lib/dynamoid/associations/has_many.rb +1 -0
- data/lib/dynamoid/associations/has_one.rb +1 -0
- data/lib/dynamoid/associations/single_association.rb +1 -0
- data/lib/dynamoid/criteria.rb +4 -4
- data/lib/dynamoid/criteria/chain.rb +86 -79
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
- data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
- data/lib/dynamoid/document.rb +18 -13
- data/lib/dynamoid/dumping.rb +52 -40
- data/lib/dynamoid/fields.rb +4 -3
- data/lib/dynamoid/finders.rb +3 -3
- data/lib/dynamoid/persistence.rb +5 -6
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
- data/lib/dynamoid/tasks.rb +1 -0
- data/lib/dynamoid/tasks/database.rake +2 -2
- data/lib/dynamoid/type_casting.rb +37 -19
- data/lib/dynamoid/undumping.rb +53 -42
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +17 -5
- data/lib/dynamoid/adapter_plugin/query.rb +0 -144
- data/lib/dynamoid/adapter_plugin/scan.rb +0 -107
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 193003bce5df92dae1a2ad3a4f94345bc610193b0da7692eb6f722231e1becae
|
4
|
+
data.tar.gz: 36fb0bdea8126c6fd9af32343ad5d0ebe87ca92d01caab17fb9fe87aa74d07a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c53982905e1e487db42f0497f682e4a62b33d8adf1b1b7079e98f5d1635e338396aa62bef467483406ab558d4ad66a2a285ac72c90d5f278530026fbcb96fa1
|
7
|
+
data.tar.gz: 2d8e83e5ea4ba495ac6ec37c7178b8f77db4d0b2e33210c698ef00f8f92f1762672df3ca64c70e053f173d040e2abafe01bdd0c850499f65523fb9a50c2f0a7b
|
data/.rubocop.yml
CHANGED
@@ -8,12 +8,28 @@ AllCops:
|
|
8
8
|
# It's a matter of taste
|
9
9
|
Layout/AlignParameters:
|
10
10
|
EnforcedStyle: with_fixed_indentation
|
11
|
+
Layout/AlignHash:
|
12
|
+
Enabled: false
|
11
13
|
Style/GuardClause:
|
12
14
|
Enabled: false
|
13
15
|
Style/FormatStringToken:
|
14
16
|
Enabled: false
|
15
17
|
Style/DoubleNegation:
|
16
18
|
Enabled: false
|
19
|
+
Style/IfUnlessModifier:
|
20
|
+
Enabled: false
|
21
|
+
Style/EachWithObject:
|
22
|
+
Enabled: false
|
23
|
+
Style/SafeNavigation:
|
24
|
+
Enabled: false
|
25
|
+
Style/BlockDelimiters:
|
26
|
+
Enabled: false
|
27
|
+
Layout/MultilineMethodCallIndentation:
|
28
|
+
EnforcedStyle: indented
|
29
|
+
Naming/VariableNumber:
|
30
|
+
Enabled: false
|
31
|
+
Style/MultilineBlockChain:
|
32
|
+
Enabled: false
|
17
33
|
|
18
34
|
# We aren't so brave to tackle all these issues right now
|
19
35
|
Metrics/LineLength:
|
@@ -50,4 +66,6 @@ Style/MissingRespondToMissing:
|
|
50
66
|
Enabled: false
|
51
67
|
Naming/PredicateName:
|
52
68
|
Enabled: false
|
69
|
+
Security/YAMLLoad:
|
70
|
+
Enabled: false
|
53
71
|
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -10,6 +10,21 @@
|
|
10
10
|
|
11
11
|
|
12
12
|
|
13
|
+
# 3.2.0
|
14
|
+
|
15
|
+
## Features
|
16
|
+
* Feature: [#341](https://github.com/Dynamoid/dynamoid/pull/341), [#342](https://github.com/Dynamoid/dynamoid/pull/342) Add `find_by_pages` method to provide access to DynamoDB query result pagination mechanism (@bmalinconico, @arjes)
|
17
|
+
* Feature: [#354](https://github.com/Dynamoid/dynamoid/pull/354) Add `map` field type
|
18
|
+
|
19
|
+
## Improvements
|
20
|
+
* Improvement: [#340](https://github.com/Dynamoid/dynamoid/pull/340) Improve selecting more optimal GSI for Query operation - choose GSI with sort key if it's used in criteria (@ryz310)
|
21
|
+
* Improvement: [#351](https://github.com/Dynamoid/dynamoid/pull/351) Add warnings about nonexistent fields in `where` conditions
|
22
|
+
* Improvement: [#352](https://github.com/Dynamoid/dynamoid/pull/352) Add warning about skipped conditions
|
23
|
+
* Improvement: [#356](https://github.com/Dynamoid/dynamoid/pull/356) Simplify requiring Rake tasks in non-Rails application
|
24
|
+
* Improvement: Readme.md. Minor improvements and fixes (@cabello)
|
25
|
+
|
26
|
+
|
27
|
+
|
13
28
|
# 3.1.0
|
14
29
|
|
15
30
|
## Improvements
|
data/README.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# Dynamoid
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
[](https://travis-ci.org/Dynamoid/dynamoid)
|
4
|
+
[](https://codeclimate.com/github/Dynamoid/dynamoid)
|
5
|
+
[](https://coveralls.io/github/Dynamoid/dynamoid?branch=master)
|
6
|
+
[](https://www.codetriage.com/dynamoid/dynamoid)
|
7
|
+
[](https://www.rubydoc.info/github/Dynamoid/dynamoid/frames)
|
8
|
+
[](http://inch-ci.org/github/Dynamoid/Dynamoid)
|
9
|
+

|
6
10
|
|
7
11
|
Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
|
8
12
|
provides similar functionality to ActiveRecord and improves on
|
@@ -14,27 +18,12 @@ DynamoDB is not like other document-based databases you might know, and is very
|
|
14
18
|
|
15
19
|
But if you want a fast, scalable, simple, easy-to-use database (and a Gem that supports it) then look no further!
|
16
20
|
|
17
|
-
|
18
|
-
| Project | Dynamoid |
|
19
|
-
|------------------------ | ----------------- |
|
20
|
-
| gem name | dynamoid |
|
21
|
-
| license | MIT |
|
22
|
-
| download rank | [](https://rubygems.org/gems/dynamoid) |
|
23
|
-
| version | [](https://badge.fury.io/rb/dynamoid) |
|
24
|
-
| dependencies | [](https://depfu.com) |
|
25
|
-
| code quality | [](https://codeclimate.com/github/Dynamoid/dynamoid) |
|
26
|
-
| continuous integration | [](https://travis-ci.org/Dynamoid/dynamoid) |
|
27
|
-
| test coverage | [](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
|
28
|
-
| triage helpers | [](https://www.codetriage.com/dynamoid/dynamoid) |
|
29
|
-
| homepage | [https://github.com/Dynamoid/dynamoid](https://github.com/Dynamoid/dynamoid) |
|
30
|
-
| documentation | [http://rdoc.info/github/Dynamoid/dynamoid/frames](http://rdoc.info/github/Dynamoid/dynamoid/frames) |
|
31
|
-
|
32
21
|
## Installation
|
33
22
|
|
34
23
|
Installing Dynamoid is pretty simple. First include the Gem in your Gemfile:
|
35
24
|
|
36
25
|
```ruby
|
37
|
-
gem 'dynamoid'
|
26
|
+
gem 'dynamoid'
|
38
27
|
```
|
39
28
|
## Prerequisities
|
40
29
|
|
@@ -98,14 +87,9 @@ Then you need to initialize Dynamoid config to get it going. Put code similar to
|
|
98
87
|
|
99
88
|
Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
|
100
89
|
|
101
|
-
Its compatibility is tested
|
102
|
-
|
103
|
-
|
104
|
-
|:---------------------:|:-----:|:-----:|:-----:|:-----:|
|
105
|
-
| 2.3.7 | ✓ | ✓ | ✓ | ✓ |
|
106
|
-
| 2.4.4 | ✓ | ✓ | ✓ | ✓ |
|
107
|
-
| 2.5.1 | ✓ | ✓ | ✓ | ✓ |
|
108
|
-
| jruby-9.1.17.0 | ✓ | ✓ | ✓ | ✓ |
|
90
|
+
Its compatibility is tested against following Ruby versions: 2.3.8,
|
91
|
+
2.4.5, 2.5.3 and 2.6.1, JRuby versions 9.1.17.0 and 9.2.6.0 and
|
92
|
+
against Rails versions: 4.2.x, 5.0.x, 5.1.x and 5.2.x.
|
109
93
|
|
110
94
|
## Setup
|
111
95
|
|
@@ -137,7 +121,8 @@ These fields will not change an existing table: so specifying a new read_capacit
|
|
137
121
|
You'll have to define all the fields on the model and the data type of each field. Every field on the object must be included here; if you miss any they'll be completely bypassed during DynamoDB's initialization and will not appear on the model objects.
|
138
122
|
|
139
123
|
By default, fields are assumed to be of type `:string`. Other built-in types are
|
140
|
-
`:integer`, `:number`, `:set`, `:array`, `:datetime`, `date`, `:boolean`, `:raw` and `:serialized`.
|
124
|
+
`:integer`, `:number`, `:set`, `:array`, `:map`, `:datetime`, `date`, `:boolean`, `:raw` and `:serialized`.
|
125
|
+
`array` and `map` match List and Map DynamoDB types respectively.
|
141
126
|
`raw` type means you can store Ruby Array, Hash, String and numbers.
|
142
127
|
If built-in types do not suit you, you can use a custom field type represented by an arbitrary class, provided that the class supports a compatible serialization interface.
|
143
128
|
The primary use case for using a custom field type is to represent your business logic with high-level types, while ensuring portability or backward-compatibility of the serialized representation.
|
@@ -181,7 +166,7 @@ class Document
|
|
181
166
|
end
|
182
167
|
```
|
183
168
|
|
184
|
-
WARNING
|
169
|
+
**WARNING:** Fields in numeric format are stored with nanoseconds as a fraction part and precision could be lost.
|
185
170
|
That's why `datetime` field in numeric format shouldn't be used as a range key.
|
186
171
|
|
187
172
|
You have two options if you need to use a `datetime` field as a range key:
|
@@ -547,6 +532,18 @@ u.addresses.where(city: 'Chicago').all
|
|
547
532
|
|
548
533
|
But keep in mind Dynamoid -- and document-based storage systems in general -- are not drop-in replacements for existing relational databases. The above query does not efficiently perform a conditional join, but instead finds all the user's addresses and naively filters them in Ruby. For large associations this is a performance hit compared to relational database engines.
|
549
534
|
|
535
|
+
**WARNING:** There is a limitation of conditions passed to `where`
|
536
|
+
method. Only one condition for some particular field could be specified.
|
537
|
+
The last one only will be applyed and others will be ignored. E.g. in
|
538
|
+
examples:
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
User.where('age.gt': 10, 'age.lt': 20)
|
542
|
+
User.where(name: 'Mike').where('name.begins_with': 'Ed')
|
543
|
+
```
|
544
|
+
|
545
|
+
the first one will be ignored and the last one will be used.
|
546
|
+
|
550
547
|
#### Limits
|
551
548
|
|
552
549
|
There are three types of limits that you can query with:
|
@@ -585,6 +582,42 @@ Address.record_limit(10_000).batch(100).each { … } # Batch specified as part o
|
|
585
582
|
The implication of batches is that the underlying requests are done in the batch sizes to make the request and responses
|
586
583
|
more manageable. Note that this batching is for `Query` and `Scans` and not `BatchGetItem` commands.
|
587
584
|
|
585
|
+
#### DynamoDB pagination
|
586
|
+
|
587
|
+
At times it can be useful to rely on DynamoDB [low-level pagination](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Pagination)
|
588
|
+
instead of fixed pages sizes. Each page results in a single Query or Scan call
|
589
|
+
to DyanmoDB, but returns an unknown number of records.
|
590
|
+
|
591
|
+
Access to the native DynamoDB pages can be obtained via the `find_by_pages`
|
592
|
+
method, which yields arrays of records.
|
593
|
+
|
594
|
+
```ruby
|
595
|
+
Address.find_by_pages do |addresses, metadata|
|
596
|
+
end
|
597
|
+
```
|
598
|
+
|
599
|
+
Each yielded pages returns page metadata as the second argument, which is a hash
|
600
|
+
including a key `:last_evaluated_key`. The value of this key can be used for
|
601
|
+
the `start` method to fetch the next page of records.
|
602
|
+
|
603
|
+
This way it can be used for instance to implement efficiently
|
604
|
+
pagination in web-application:
|
605
|
+
|
606
|
+
```ruby
|
607
|
+
class UserController < ApplicationController
|
608
|
+
def index
|
609
|
+
next_page = params[:next_page_token] ? JSON.parse(Base64.decode64(params[:next_page_token])) : nil
|
610
|
+
|
611
|
+
records, metadata = User.start(next_page).find_by_pages.first
|
612
|
+
|
613
|
+
render json: {
|
614
|
+
records: records,
|
615
|
+
next_page_token: Base64.encode64(metadata[:last_evaluated_key].to_json)
|
616
|
+
}
|
617
|
+
end
|
618
|
+
end
|
619
|
+
```
|
620
|
+
|
588
621
|
#### Sort Conditions and Filters
|
589
622
|
|
590
623
|
You are able to optimize query with condition for sort key. Following operators are available: `gt`, `lt`, `gte`, `lte`,
|
@@ -684,7 +717,7 @@ class User
|
|
684
717
|
field :name
|
685
718
|
field :age, :number
|
686
719
|
|
687
|
-
global_secondary_index hash_key: :age
|
720
|
+
global_secondary_index hash_key: :age # Must come after field definitions.
|
688
721
|
end
|
689
722
|
```
|
690
723
|
|
@@ -698,44 +731,17 @@ There are following options:
|
|
698
731
|
|
699
732
|
The only mandatory option is `name`.
|
700
733
|
|
701
|
-
|
702
|
-
|
703
|
-
There are two ways to query Global Secondary Indexes (GSI).
|
704
|
-
|
705
|
-
#### Explicit
|
706
|
-
|
707
|
-
The first way explicitly uses your GSI and utilizes the `find_all_by_secondary_index` method which will lookup a valid
|
708
|
-
GSI to use based on the inputs, you MUST provide the correct keys to match the GSI you want:
|
734
|
+
**WARNING:** In order to use global secondary index in `Document.where` implicitly you need to have all the attributes of the original table in the index and declare it with option `projected_attributes: :all`:
|
709
735
|
|
710
736
|
```ruby
|
711
|
-
|
712
|
-
|
713
|
-
dynamo_primary_key_column_name => dynamo_primary_key_value
|
714
|
-
}, # The signature of find_all_by_secondary_index is ugly, so must be an explicit hash here
|
715
|
-
:range => {
|
716
|
-
"#{range_column}.#{range_modifier}" => range_value
|
717
|
-
},
|
718
|
-
# false is the same as DESC in SQL (newest timestamp first)
|
719
|
-
# true is the same as ASC in SQL (oldest timestamp first)
|
720
|
-
scan_index_forward: false # or true
|
721
|
-
)
|
722
|
-
```
|
723
|
-
|
724
|
-
Where the range modifier is one of `Dynamoid::Finders::RANGE_MAP.keys`, where the `RANGE_MAP` is:
|
737
|
+
class User
|
738
|
+
# ...
|
725
739
|
|
726
|
-
|
727
|
-
|
728
|
-
'gt' => :range_greater_than,
|
729
|
-
'lt' => :range_less_than,
|
730
|
-
'gte' => :range_gte,
|
731
|
-
'lte' => :range_lte,
|
732
|
-
'begins_with' => :range_begins_with,
|
733
|
-
'between' => :range_between,
|
734
|
-
'eq' => :range_eq
|
735
|
-
}
|
740
|
+
global_secondary_index hash_key: :age, projected_attributes: :all
|
741
|
+
end
|
736
742
|
```
|
737
743
|
|
738
|
-
|
744
|
+
There is only one implicit way to query Global and Local Secondary Indexes (GSI/LSI).
|
739
745
|
|
740
746
|
#### Implicit
|
741
747
|
|
@@ -791,6 +797,10 @@ Listed below are all configuration options.
|
|
791
797
|
`'t'` and `'f'`. Default is true
|
792
798
|
* `backoff` - is a hash: key is a backoff strategy (symbol), value is parameters for the strategy. Is used in batch operations. Default id `nil`
|
793
799
|
* `backoff_strategies`: is a hash and contains all available strategies. Default is { constant: ..., exponential: ...}
|
800
|
+
* `http_continue_timeout`: The number of seconds to wait for a 100-continue HTTP response before sending the request body. Default option value is `nil`. If not specified effected value is `1`
|
801
|
+
* `http_idle_timeout`: The number of seconds an HTTP connection is allowed to sit idble before it is considered stale. Default option value is `nil`. If not specified effected value is `5`
|
802
|
+
* `http_open_timeout`: The number of seconds to wait when opening a HTTP session. Default option value is `nil`. If not specified effected value is `15`
|
803
|
+
* `http_read_timeout`:The number of seconds to wait for HTTP response data. Default option value is `nil`. If not specified effected value is `60`
|
794
804
|
|
795
805
|
|
796
806
|
## Concurrency
|
@@ -859,9 +869,23 @@ end
|
|
859
869
|
|
860
870
|
## Rake Tasks
|
861
871
|
|
872
|
+
There are a few Rake tasks available out of the box:
|
873
|
+
|
862
874
|
* `rake dynamoid:create_tables`
|
863
875
|
* `rake dynamoid:ping`
|
864
876
|
|
877
|
+
In order to use them in non-Rails application they should be required explicitly:
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
# Rakefile
|
881
|
+
|
882
|
+
Rake::Task.define_task(:environment)
|
883
|
+
require 'dynamoid/tasks'
|
884
|
+
```
|
885
|
+
|
886
|
+
The Rake tasks depend on `:environment` task so it should be declared
|
887
|
+
as well.
|
888
|
+
|
865
889
|
## Test Environment
|
866
890
|
|
867
891
|
In test environment you will most likely want to clean the database between test runs to keep tests completely isolated. This can be achieved like so
|
@@ -904,6 +928,32 @@ Dynamoid.configure do |config|
|
|
904
928
|
end
|
905
929
|
```
|
906
930
|
|
931
|
+
## Logging
|
932
|
+
|
933
|
+
There is a config option `logger`. Dynamoid writes requests and
|
934
|
+
responses to DynamoDB using this logger on the `debug` level. So in
|
935
|
+
order to troubleshoot and debug issues just set it:
|
936
|
+
|
937
|
+
```ruby
|
938
|
+
class User
|
939
|
+
include Dynamoid::Document
|
940
|
+
field name
|
941
|
+
end
|
942
|
+
|
943
|
+
Dynamoid.config.logger.level = :debug
|
944
|
+
Dynamoid.config.endpoint = 'localhost:8000'
|
945
|
+
|
946
|
+
User.create(name: 'Alex')
|
947
|
+
|
948
|
+
# => D, [2019-05-12T20:01:07.840051 #75059] DEBUG -- : put_item | Request "{\"TableName\":\"dynamoid_users\",\"Item\":{\"created_at\":{\"N\":\"1557680467.608749\"},\"updated_at\":{\"N\":\"1557680467.608809\"},\"id\":{\"S\":\"1227eea7-2c96-4b8a-90d9-77b38eb85cd0\"}},\"Expected\":{\"id\":{\"Exists\":false}}}" | Response "{}"
|
949
|
+
|
950
|
+
# => D, [2019-05-12T20:01:07.842397 #75059] DEBUG -- : (231.28 ms) PUT ITEM - ["dynamoid_users", {:created_at=>0.1557680467608749e10, :updated_at=>0.1557680467608809e10, :id=>"1227eea7-2c96-4b8a-90d9-77b38eb85cd0", :User=>nil}, {}]
|
951
|
+
```
|
952
|
+
|
953
|
+
The first line is a body of HTTP request and response. The second line -
|
954
|
+
Dynamoid internal logging of API call (`PUT ITEM` in our case) with
|
955
|
+
timing (231.28 ms).
|
956
|
+
|
907
957
|
## Credits
|
908
958
|
|
909
959
|
Dynamoid borrows code, structure, and even its name very liberally from the truly amazing [Mongoid](https://github.com/mongoid/mongoid). Without Mongoid to crib from none of this would have been possible, and I hope they don't mind me reusing their very awesome ideas to make DynamoDB just as accessible to the Ruby world as MongoDB.
|
data/Vagrantfile
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Vagrant.configure('2') do |config|
|
4
4
|
# Choose base box
|
5
|
-
config.vm.box = 'bento/ubuntu-
|
5
|
+
config.vm.box = 'bento/ubuntu-18.04'
|
6
6
|
|
7
7
|
config.vm.provider 'virtualbox' do |vb|
|
8
8
|
# Prevent clock skew when host goes to sleep while VM is running
|
@@ -20,7 +20,7 @@ Vagrant.configure('2') do |config|
|
|
20
20
|
# Pillars
|
21
21
|
salt.pillar(
|
22
22
|
'ruby' => {
|
23
|
-
'version' => '2.
|
23
|
+
'version' => '2.6.2'
|
24
24
|
}
|
25
25
|
)
|
26
26
|
|
data/docker-compose.yml
CHANGED
data/gemfiles/rails_4_2.gemfile
CHANGED
data/gemfiles/rails_5_0.gemfile
CHANGED
data/gemfiles/rails_5_1.gemfile
CHANGED
data/gemfiles/rails_5_2.gemfile
CHANGED
data/lib/dynamoid/adapter.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'query'
|
4
|
-
require_relative 'scan'
|
3
|
+
require_relative 'aws_sdk_v3/query'
|
4
|
+
require_relative 'aws_sdk_v3/scan'
|
5
|
+
require_relative 'aws_sdk_v3/create_table'
|
6
|
+
require_relative 'aws_sdk_v3/item_updater'
|
7
|
+
require_relative 'aws_sdk_v3/table'
|
8
|
+
require_relative 'aws_sdk_v3/until_past_table_status'
|
5
9
|
|
6
10
|
module Dynamoid
|
7
11
|
module AdapterPlugin
|
@@ -187,6 +191,7 @@ module Dynamoid
|
|
187
191
|
|
188
192
|
table_ids.each do |t, ids|
|
189
193
|
next if ids.blank?
|
194
|
+
|
190
195
|
ids = Array(ids).dup
|
191
196
|
tbl = describe_table(t)
|
192
197
|
hk = tbl.hash_key.to_s
|
@@ -293,58 +298,7 @@ module Dynamoid
|
|
293
298
|
# @since 1.0.0
|
294
299
|
def create_table(table_name, key = :id, options = {})
|
295
300
|
Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
|
296
|
-
|
297
|
-
write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
|
298
|
-
|
299
|
-
secondary_indexes = options.slice(
|
300
|
-
:local_secondary_indexes,
|
301
|
-
:global_secondary_indexes
|
302
|
-
)
|
303
|
-
ls_indexes = options[:local_secondary_indexes]
|
304
|
-
gs_indexes = options[:global_secondary_indexes]
|
305
|
-
|
306
|
-
key_schema = {
|
307
|
-
hash_key_schema: { key => (options[:hash_key_type] || :string) },
|
308
|
-
range_key_schema: options[:range_key]
|
309
|
-
}
|
310
|
-
attribute_definitions = build_all_attribute_definitions(
|
311
|
-
key_schema,
|
312
|
-
secondary_indexes
|
313
|
-
)
|
314
|
-
key_schema = aws_key_schema(
|
315
|
-
key_schema[:hash_key_schema],
|
316
|
-
key_schema[:range_key_schema]
|
317
|
-
)
|
318
|
-
|
319
|
-
client_opts = {
|
320
|
-
table_name: table_name,
|
321
|
-
provisioned_throughput: {
|
322
|
-
read_capacity_units: read_capacity,
|
323
|
-
write_capacity_units: write_capacity
|
324
|
-
},
|
325
|
-
key_schema: key_schema,
|
326
|
-
attribute_definitions: attribute_definitions
|
327
|
-
}
|
328
|
-
|
329
|
-
if ls_indexes.present?
|
330
|
-
client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
|
331
|
-
index_to_aws_hash(index)
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
if gs_indexes.present?
|
336
|
-
client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
|
337
|
-
index_to_aws_hash(index)
|
338
|
-
end
|
339
|
-
end
|
340
|
-
resp = client.create_table(client_opts)
|
341
|
-
options[:sync] = true if !options.key?(:sync) && ls_indexes.present? || gs_indexes.present?
|
342
|
-
until_past_table_status(table_name, :creating) if options[:sync] &&
|
343
|
-
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
344
|
-
status == TABLE_STATUSES[:creating]
|
345
|
-
# Response to original create_table, which, if options[:sync]
|
346
|
-
# may have an outdated table_description.table_status of "CREATING"
|
347
|
-
resp
|
301
|
+
CreateTable.new(client, table_name, key, options).call
|
348
302
|
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
349
303
|
Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
|
350
304
|
end
|
@@ -402,9 +356,9 @@ module Dynamoid
|
|
402
356
|
# @since 1.0.0
|
403
357
|
def delete_table(table_name, options = {})
|
404
358
|
resp = client.delete_table(table_name: table_name)
|
405
|
-
|
406
|
-
|
407
|
-
|
359
|
+
UntilPastTableStatus.new(table_name, :deleting).call if options[:sync] &&
|
360
|
+
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
361
|
+
status == TABLE_STATUSES[:deleting]
|
408
362
|
table_cache.delete(table_name)
|
409
363
|
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
410
364
|
Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
|
@@ -461,6 +415,7 @@ module Dynamoid
|
|
461
415
|
yield(iu = ItemUpdater.new(table, key, range_key))
|
462
416
|
|
463
417
|
raise "non-empty options: #{options}" unless options.empty?
|
418
|
+
|
464
419
|
begin
|
465
420
|
result = client.update_item(table_name: table_name,
|
466
421
|
key: key_stanza(table, key, range_key),
|
@@ -532,11 +487,14 @@ module Dynamoid
|
|
532
487
|
#
|
533
488
|
# @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
|
534
489
|
def query(table_name, options = {})
|
535
|
-
table = describe_table(table_name)
|
536
|
-
|
537
490
|
Enumerator.new do |yielder|
|
491
|
+
table = describe_table(table_name)
|
492
|
+
|
538
493
|
Query.new(client, table, options).call.each do |page|
|
539
|
-
|
494
|
+
yielder.yield(
|
495
|
+
page.items.map { |row| result_item_to_hash(row) },
|
496
|
+
last_evaluated_key: page.last_evaluated_key
|
497
|
+
)
|
540
498
|
end
|
541
499
|
end
|
542
500
|
end
|
@@ -562,11 +520,14 @@ module Dynamoid
|
|
562
520
|
#
|
563
521
|
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
564
522
|
def scan(table_name, conditions = {}, options = {})
|
565
|
-
table = describe_table(table_name)
|
566
|
-
|
567
523
|
Enumerator.new do |yielder|
|
524
|
+
table = describe_table(table_name)
|
525
|
+
|
568
526
|
Scan.new(client, table, conditions, options).call.each do |page|
|
569
|
-
|
527
|
+
yielder.yield(
|
528
|
+
page.items.map { |row| result_item_to_hash(row) },
|
529
|
+
last_evaluated_key: page.last_evaluated_key
|
530
|
+
)
|
570
531
|
end
|
571
532
|
end
|
572
533
|
end
|
@@ -591,7 +552,7 @@ module Dynamoid
|
|
591
552
|
hk = table.hash_key
|
592
553
|
rk = table.range_key
|
593
554
|
|
594
|
-
scan(table_name, {}, {}).each do |attributes|
|
555
|
+
scan(table_name, {}, {}).flat_map{ |i| i }.each do |attributes|
|
595
556
|
opts = {}
|
596
557
|
opts[:range_key] = attributes[rk.to_sym] if rk
|
597
558
|
delete_item(table_name, attributes[hk], opts)
|
@@ -604,59 +565,6 @@ module Dynamoid
|
|
604
565
|
|
605
566
|
protected
|
606
567
|
|
607
|
-
def check_table_status?(counter, resp, expect_status)
|
608
|
-
status = PARSE_TABLE_STATUS.call(resp)
|
609
|
-
again = counter < Dynamoid::Config.sync_retry_max_times &&
|
610
|
-
status == TABLE_STATUSES[expect_status]
|
611
|
-
{ again: again, status: status, counter: counter }
|
612
|
-
end
|
613
|
-
|
614
|
-
def until_past_table_status(table_name, status = :creating)
|
615
|
-
counter = 0
|
616
|
-
resp = nil
|
617
|
-
begin
|
618
|
-
check = { again: true }
|
619
|
-
while check[:again]
|
620
|
-
sleep Dynamoid::Config.sync_retry_wait_seconds
|
621
|
-
resp = client.describe_table(table_name: table_name)
|
622
|
-
check = check_table_status?(counter, resp, status)
|
623
|
-
Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
|
624
|
-
counter += 1
|
625
|
-
end
|
626
|
-
# If you issue a DescribeTable request immediately after a CreateTable
|
627
|
-
# request, DynamoDB might return a ResourceNotFoundException.
|
628
|
-
# This is because DescribeTable uses an eventually consistent query,
|
629
|
-
# and the metadata for your table might not be available at that moment.
|
630
|
-
# Wait for a few seconds, and then try the DescribeTable request again.
|
631
|
-
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method
|
632
|
-
rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
|
633
|
-
case status
|
634
|
-
when :creating then
|
635
|
-
if counter >= Dynamoid::Config.sync_retry_max_times
|
636
|
-
Dynamoid.logger.warn "Waiting on table metadata for #{table_name} (check #{counter})"
|
637
|
-
retry # start over at first line of begin, does not reset counter
|
638
|
-
else
|
639
|
-
Dynamoid.logger.error "Exhausted max retries (Dynamoid::Config.sync_retry_max_times) waiting on table metadata for #{table_name} (check #{counter})"
|
640
|
-
raise e
|
641
|
-
end
|
642
|
-
else
|
643
|
-
# When deleting a table, "not found" is the goal.
|
644
|
-
Dynamoid.logger.info "Checked table status for #{table_name}: Not Found (check #{check.inspect})"
|
645
|
-
end
|
646
|
-
end
|
647
|
-
end
|
648
|
-
|
649
|
-
# Converts from symbol to the API string for the given data type
|
650
|
-
# E.g. :number -> 'N'
|
651
|
-
def api_type(type)
|
652
|
-
case type
|
653
|
-
when :string then STRING_TYPE
|
654
|
-
when :number then NUM_TYPE
|
655
|
-
when :binary then BINARY_TYPE
|
656
|
-
else raise "Unknown type: #{type}"
|
657
|
-
end
|
658
|
-
end
|
659
|
-
|
660
568
|
#
|
661
569
|
# The key hash passed on get_item, put_item, delete_item, update_item, etc
|
662
570
|
#
|
@@ -706,152 +614,6 @@ module Dynamoid
|
|
706
614
|
end
|
707
615
|
end
|
708
616
|
|
709
|
-
# Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
|
710
|
-
# This resulting hash is of the form:
|
711
|
-
#
|
712
|
-
# {
|
713
|
-
# index_name: String
|
714
|
-
# keys: {
|
715
|
-
# hash_key: aws_key_schema (hash)
|
716
|
-
# range_key: aws_key_schema (hash)
|
717
|
-
# }
|
718
|
-
# projection: {
|
719
|
-
# projection_type: (ALL, KEYS_ONLY, INCLUDE) String
|
720
|
-
# non_key_attributes: (optional) Array
|
721
|
-
# }
|
722
|
-
# provisioned_throughput: {
|
723
|
-
# read_capacity_units: Integer
|
724
|
-
# write_capacity_units: Integer
|
725
|
-
# }
|
726
|
-
# }
|
727
|
-
#
|
728
|
-
# @param [Dynamoid::Indexes::Index] index the index.
|
729
|
-
# @return [Hash] hash representing an AWS Index definition.
|
730
|
-
def index_to_aws_hash(index)
|
731
|
-
key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
|
732
|
-
|
733
|
-
hash = {
|
734
|
-
index_name: index.name,
|
735
|
-
key_schema: key_schema,
|
736
|
-
projection: {
|
737
|
-
projection_type: index.projection_type.to_s.upcase
|
738
|
-
}
|
739
|
-
}
|
740
|
-
|
741
|
-
# If the projection type is include, specify the non key attributes
|
742
|
-
if index.projection_type == :include
|
743
|
-
hash[:projection][:non_key_attributes] = index.projected_attributes
|
744
|
-
end
|
745
|
-
|
746
|
-
# Only global secondary indexes have a separate throughput.
|
747
|
-
if index.type == :global_secondary
|
748
|
-
hash[:provisioned_throughput] = {
|
749
|
-
read_capacity_units: index.read_capacity,
|
750
|
-
write_capacity_units: index.write_capacity
|
751
|
-
}
|
752
|
-
end
|
753
|
-
hash
|
754
|
-
end
|
755
|
-
|
756
|
-
# Converts hash_key_schema and range_key_schema to aws_key_schema
|
757
|
-
# @param [Hash] hash_key_schema eg: {:id => :string}
|
758
|
-
# @param [Hash] range_key_schema eg: {:created_at => :number}
|
759
|
-
# @return [Array]
|
760
|
-
def aws_key_schema(hash_key_schema, range_key_schema)
|
761
|
-
schema = [{
|
762
|
-
attribute_name: hash_key_schema.keys.first.to_s,
|
763
|
-
key_type: HASH_KEY
|
764
|
-
}]
|
765
|
-
|
766
|
-
if range_key_schema.present?
|
767
|
-
schema << {
|
768
|
-
attribute_name: range_key_schema.keys.first.to_s,
|
769
|
-
key_type: RANGE_KEY
|
770
|
-
}
|
771
|
-
end
|
772
|
-
schema
|
773
|
-
end
|
774
|
-
|
775
|
-
# Builds aws attributes definitions based off of primary hash/range and
|
776
|
-
# secondary indexes
|
777
|
-
#
|
778
|
-
# @param key_data
|
779
|
-
# @option key_data [Hash] hash_key_schema - eg: {:id => :string}
|
780
|
-
# @option key_data [Hash] range_key_schema - eg: {:created_at => :number}
|
781
|
-
# @param [Hash] secondary_indexes
|
782
|
-
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
|
783
|
-
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
|
784
|
-
def build_all_attribute_definitions(key_schema, secondary_indexes = {})
|
785
|
-
ls_indexes = secondary_indexes[:local_secondary_indexes]
|
786
|
-
gs_indexes = secondary_indexes[:global_secondary_indexes]
|
787
|
-
|
788
|
-
attribute_definitions = []
|
789
|
-
|
790
|
-
attribute_definitions << build_attribute_definitions(
|
791
|
-
key_schema[:hash_key_schema],
|
792
|
-
key_schema[:range_key_schema]
|
793
|
-
)
|
794
|
-
|
795
|
-
if ls_indexes.present?
|
796
|
-
ls_indexes.map do |index|
|
797
|
-
attribute_definitions << build_attribute_definitions(
|
798
|
-
index.hash_key_schema,
|
799
|
-
index.range_key_schema
|
800
|
-
)
|
801
|
-
end
|
802
|
-
end
|
803
|
-
|
804
|
-
if gs_indexes.present?
|
805
|
-
gs_indexes.map do |index|
|
806
|
-
attribute_definitions << build_attribute_definitions(
|
807
|
-
index.hash_key_schema,
|
808
|
-
index.range_key_schema
|
809
|
-
)
|
810
|
-
end
|
811
|
-
end
|
812
|
-
|
813
|
-
attribute_definitions.flatten!
|
814
|
-
# uniq these definitions because range keys might be common between
|
815
|
-
# primary and secondary indexes
|
816
|
-
attribute_definitions.uniq!
|
817
|
-
attribute_definitions
|
818
|
-
end
|
819
|
-
|
820
|
-
# Builds an attribute definitions based on hash key and range key
|
821
|
-
# @params [Hash] hash_key_schema - eg: {:id => :string}
|
822
|
-
# @params [Hash] range_key_schema - eg: {:created_at => :datetime}
|
823
|
-
# @return [Array]
|
824
|
-
def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
|
825
|
-
attrs = []
|
826
|
-
|
827
|
-
attrs << attribute_definition_element(
|
828
|
-
hash_key_schema.keys.first,
|
829
|
-
hash_key_schema.values.first
|
830
|
-
)
|
831
|
-
|
832
|
-
if range_key_schema.present?
|
833
|
-
attrs << attribute_definition_element(
|
834
|
-
range_key_schema.keys.first,
|
835
|
-
range_key_schema.values.first
|
836
|
-
)
|
837
|
-
end
|
838
|
-
|
839
|
-
attrs
|
840
|
-
end
|
841
|
-
|
842
|
-
# Builds an aws attribute definition based on name and dynamoid type
|
843
|
-
# @params [Symbol] name - eg: :id
|
844
|
-
# @params [Symbol] dynamoid_type - eg: :string
|
845
|
-
# @return [Hash]
|
846
|
-
def attribute_definition_element(name, dynamoid_type)
|
847
|
-
aws_type = api_type(dynamoid_type)
|
848
|
-
|
849
|
-
{
|
850
|
-
attribute_name: name.to_s,
|
851
|
-
attribute_type: aws_type
|
852
|
-
}
|
853
|
-
end
|
854
|
-
|
855
617
|
# Build an array of values for Condition
|
856
618
|
# Is used in ScanFilter and QueryFilter
|
857
619
|
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
|
@@ -871,139 +633,8 @@ module Dynamoid
|
|
871
633
|
end
|
872
634
|
end
|
873
635
|
|
874
|
-
#
|
875
|
-
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
876
|
-
# provides methods for coercing values to the proper types based on the table's schema data
|
877
|
-
#
|
878
|
-
class Table
|
879
|
-
attr_reader :schema
|
880
|
-
|
881
|
-
#
|
882
|
-
# @param [Hash] schema Data returns from a "DescribeTable" call
|
883
|
-
#
|
884
|
-
def initialize(schema)
|
885
|
-
@schema = schema[:table]
|
886
|
-
end
|
887
|
-
|
888
|
-
def range_key
|
889
|
-
@range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
|
890
|
-
end
|
891
|
-
|
892
|
-
def range_type
|
893
|
-
range_type ||= schema[:attribute_definitions].find do |d|
|
894
|
-
d[:attribute_name] == range_key
|
895
|
-
end.try(:fetch, :attribute_type, nil)
|
896
|
-
end
|
897
|
-
|
898
|
-
def hash_key
|
899
|
-
@hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
|
900
|
-
end
|
901
|
-
|
902
|
-
#
|
903
|
-
# Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
|
904
|
-
# nil otherwise
|
905
|
-
#
|
906
|
-
def col_type(col)
|
907
|
-
col = col.to_s
|
908
|
-
col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
|
909
|
-
col_def && col_def[:attribute_type]
|
910
|
-
end
|
911
|
-
|
912
|
-
def item_count
|
913
|
-
schema[:item_count]
|
914
|
-
end
|
915
|
-
|
916
|
-
def name
|
917
|
-
schema[:table_name]
|
918
|
-
end
|
919
|
-
end
|
920
|
-
|
921
|
-
#
|
922
|
-
# Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
|
923
|
-
#
|
924
|
-
class ItemUpdater
|
925
|
-
attr_reader :table, :key, :range_key
|
926
|
-
|
927
|
-
def initialize(table, key, range_key = nil)
|
928
|
-
@table = table
|
929
|
-
@key = key
|
930
|
-
@range_key = range_key
|
931
|
-
@additions = {}
|
932
|
-
@deletions = {}
|
933
|
-
@updates = {}
|
934
|
-
end
|
935
|
-
|
936
|
-
#
|
937
|
-
# Adds the given values to the values already stored in the corresponding columns.
|
938
|
-
# The column must contain a Set or a number.
|
939
|
-
#
|
940
|
-
# @param [Hash] vals keys of the hash are the columns to update, vals are the values to
|
941
|
-
# add. values must be a Set, Array, or Numeric
|
942
|
-
#
|
943
|
-
def add(values)
|
944
|
-
@additions.merge!(sanitize_attributes(values))
|
945
|
-
end
|
946
|
-
|
947
|
-
#
|
948
|
-
# Removes values from the sets of the given columns
|
949
|
-
#
|
950
|
-
# @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
|
951
|
-
# to remove
|
952
|
-
#
|
953
|
-
def delete(values)
|
954
|
-
@deletions.merge!(sanitize_attributes(values))
|
955
|
-
end
|
956
|
-
|
957
|
-
#
|
958
|
-
# Replaces the values of one or more attributes
|
959
|
-
#
|
960
|
-
def set(values)
|
961
|
-
@updates.merge!(sanitize_attributes(values))
|
962
|
-
end
|
963
|
-
|
964
|
-
#
|
965
|
-
# Returns an AttributeUpdates hash suitable for passing to the V2 Client API
|
966
|
-
#
|
967
|
-
def to_h
|
968
|
-
ret = {}
|
969
|
-
|
970
|
-
@additions.each do |k, v|
|
971
|
-
ret[k.to_s] = {
|
972
|
-
action: ADD,
|
973
|
-
value: v
|
974
|
-
}
|
975
|
-
end
|
976
|
-
@deletions.each do |k, v|
|
977
|
-
ret[k.to_s] = {
|
978
|
-
action: DELETE,
|
979
|
-
value: v
|
980
|
-
}
|
981
|
-
end
|
982
|
-
@updates.each do |k, v|
|
983
|
-
ret[k.to_s] = {
|
984
|
-
action: PUT,
|
985
|
-
value: v
|
986
|
-
}
|
987
|
-
end
|
988
|
-
|
989
|
-
ret
|
990
|
-
end
|
991
|
-
|
992
|
-
private
|
993
|
-
|
994
|
-
def sanitize_attributes(attributes)
|
995
|
-
attributes.transform_values do |v|
|
996
|
-
v.is_a?(Hash) ? v.stringify_keys : v
|
997
|
-
end
|
998
|
-
end
|
999
|
-
|
1000
|
-
ADD = 'ADD'
|
1001
|
-
DELETE = 'DELETE'
|
1002
|
-
PUT = 'PUT'
|
1003
|
-
end
|
1004
|
-
|
1005
636
|
def sanitize_item(attributes)
|
1006
|
-
attributes.reject do |
|
637
|
+
attributes.reject do |_, v|
|
1007
638
|
v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
1008
639
|
end.transform_values do |v|
|
1009
640
|
v.is_a?(Hash) ? v.stringify_keys : v
|