dynamoid 3.1.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +18 -0
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +15 -0
  5. data/README.md +113 -63
  6. data/Vagrantfile +2 -2
  7. data/docker-compose.yml +1 -1
  8. data/gemfiles/rails_4_2.gemfile +1 -1
  9. data/gemfiles/rails_5_0.gemfile +1 -1
  10. data/gemfiles/rails_5_1.gemfile +1 -1
  11. data/gemfiles/rails_5_2.gemfile +1 -1
  12. data/lib/dynamoid/adapter.rb +1 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
  17. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
  18. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
  19. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
  20. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
  21. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
  22. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
  24. data/lib/dynamoid/associations/has_many.rb +1 -0
  25. data/lib/dynamoid/associations/has_one.rb +1 -0
  26. data/lib/dynamoid/associations/single_association.rb +1 -0
  27. data/lib/dynamoid/criteria.rb +4 -4
  28. data/lib/dynamoid/criteria/chain.rb +86 -79
  29. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
  30. data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
  31. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
  32. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
  33. data/lib/dynamoid/document.rb +18 -13
  34. data/lib/dynamoid/dumping.rb +52 -40
  35. data/lib/dynamoid/fields.rb +4 -3
  36. data/lib/dynamoid/finders.rb +3 -3
  37. data/lib/dynamoid/persistence.rb +5 -6
  38. data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
  39. data/lib/dynamoid/tasks.rb +1 -0
  40. data/lib/dynamoid/tasks/database.rake +2 -2
  41. data/lib/dynamoid/type_casting.rb +37 -19
  42. data/lib/dynamoid/undumping.rb +53 -42
  43. data/lib/dynamoid/validations.rb +2 -0
  44. data/lib/dynamoid/version.rb +1 -1
  45. metadata +17 -5
  46. data/lib/dynamoid/adapter_plugin/query.rb +0 -144
  47. data/lib/dynamoid/adapter_plugin/scan.rb +0 -107
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ec92a10397fa1be691eaebe98659952c7909ab97
4
- data.tar.gz: 766016509f22d0ce3f4fe93ca68342ed6038d0b9
2
+ SHA256:
3
+ metadata.gz: 193003bce5df92dae1a2ad3a4f94345bc610193b0da7692eb6f722231e1becae
4
+ data.tar.gz: 36fb0bdea8126c6fd9af32343ad5d0ebe87ca92d01caab17fb9fe87aa74d07a6
5
5
  SHA512:
6
- metadata.gz: 6b0de822f2cda614a74a933c714072b97525a7b1a03444f2b12034327a8769d61e13220eb9d0b0f7ec15b2fbc2bf244574efe45a0660fac26cc118bb84fe848e
7
- data.tar.gz: d6f3287d3741c21167b521d35771a4d794a9dff90047f9b46ebaa09373c8776389d4afa161c1430559b6fda4e0b9728f22a0a0007dbcdce507d8eedccc31018c
6
+ metadata.gz: 3c53982905e1e487db42f0497f682e4a62b33d8adf1b1b7079e98f5d1635e338396aa62bef467483406ab558d4ad66a2a285ac72c90d5f278530026fbcb96fa1
7
+ data.tar.gz: 2d8e83e5ea4ba495ac6ec37c7178b8f77db4d0b2e33210c698ef00f8f92f1762672df3ca64c70e053f173d040e2abafe01bdd0c850499f65523fb9a50c2f0a7b
@@ -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
 
@@ -2,10 +2,12 @@ sudo: required
2
2
 
3
3
  language: ruby
4
4
  rvm:
5
- - ruby-2.3.7
6
- - ruby-2.4.4
7
- - ruby-2.5.1
5
+ - ruby-2.3.8
6
+ - ruby-2.4.5
7
+ - ruby-2.5.3
8
+ - ruby-2.6.1
8
9
  - jruby-9.1.17.0
10
+ - jruby-9.2.6.0
9
11
  gemfile:
10
12
  gemfile:
11
13
  - gemfiles/rails_4_2.gemfile
@@ -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
- You are viewing the README for version 3 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
- For version 1.3.x use the [1-3-stable branch](https://github.com/Dynamoid/Dynamoid/blob/1-3-stable/README.md).
3
+ [![Build Status](https://travis-ci.org/Dynamoid/dynamoid.svg?branch=master)](https://travis-ci.org/Dynamoid/dynamoid)
4
+ [![Code Climate](https://codeclimate.com/github/Dynamoid/dynamoid.svg)](https://codeclimate.com/github/Dynamoid/dynamoid)
5
+ [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/dynamoid?branch=master)
6
+ [![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid)
7
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/github/Dynamoid/dynamoid/frames)
8
+ [![Inline docs](http://inch-ci.org/github/Dynamoid/Dynamoid.svg?branch=master)](http://inch-ci.org/github/Dynamoid/Dynamoid)
9
+ ![GitHub](https://img.shields.io/github/license/Dynamoid/dynamoid.svg)
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 | [![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://badge.fury.io/rb/dynamoid) |
24
- | dependencies | [![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
- | 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', '~> 2'
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 in following way:
102
-
103
- | Ruby / Active Record | 4.2.x | 5.0.x | 5.1.x | 5.2.x |
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: Fields in numeric format are stored with nanoseconds as a fraction part and precision could be lost.
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
- To use index in `Document.where` implicitly you need to project all the fields with option `projected_attributes: :all`.
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
- find_all_by_secondary_index(
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
- ```ruby
727
- RANGE_MAP = {
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
- Most range searches, like `eq`, need a single value, and searches like `between`, need an array with two values.
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Vagrant.configure('2') do |config|
4
4
  # Choose base box
5
- config.vm.box = 'bento/ubuntu-16.04'
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.4.1'
23
+ 'version' => '2.6.2'
24
24
  }
25
25
  )
26
26
 
@@ -2,6 +2,6 @@ version: '2'
2
2
 
3
3
  services:
4
4
  dynamodb:
5
- image: deangiberson/aws-dynamodb-local
5
+ image: amazon/dynamodb-local
6
6
  ports:
7
7
  - 8000:8000
@@ -4,8 +4,8 @@
4
4
 
5
5
  source 'https://rubygems.org'
6
6
 
7
+ gem 'activemodel', '~> 4.2.0'
7
8
  gem 'nokogiri', '~> 1.6.8'
8
9
  gem 'pry-byebug', platforms: :ruby
9
- gem 'rails', '~> 4.2.0'
10
10
 
11
11
  gemspec path: '../'
@@ -4,7 +4,7 @@
4
4
 
5
5
  source 'https://rubygems.org'
6
6
 
7
+ gem 'activemodel', '~> 5.0.0'
7
8
  gem 'pry-byebug', platforms: :ruby
8
- gem 'rails', '~> 5.0.0'
9
9
 
10
10
  gemspec path: '../'
@@ -4,7 +4,7 @@
4
4
 
5
5
  source 'https://rubygems.org'
6
6
 
7
+ gem 'activemodel', '~> 5.1.0'
7
8
  gem 'pry-byebug', platforms: :ruby
8
- gem 'rails', '~> 5.1.0'
9
9
 
10
10
  gemspec path: '../'
@@ -4,7 +4,7 @@
4
4
 
5
5
  source 'https://rubygems.org'
6
6
 
7
+ gem 'activemodel', '~> 5.2.0'
7
8
  gem 'pry-byebug', platforms: :ruby
8
- gem 'rails', '~> 5.2.0'
9
9
 
10
10
  gemspec path: '../'
@@ -155,6 +155,7 @@ module Dynamoid
155
155
  # @since 0.2.0
156
156
  def method_missing(method, *args, &block)
157
157
  return benchmark(method, *args) { adapter.send(method, *args, &block) } if adapter.respond_to?(method)
158
+
158
159
  super
159
160
  end
160
161
 
@@ -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
- read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
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
- until_past_table_status(table_name, :deleting) if options[:sync] &&
406
- (status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
407
- status == TABLE_STATUSES[:deleting]
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
- page.items.each { |row| yielder << result_item_to_hash(row) }
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
- page.items.each { |row| yielder << result_item_to_hash(row) }
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 |_k, v|
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