dynamoid 3.5.0 → 3.6.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -4
  3. data/README.md +24 -18
  4. data/lib/dynamoid.rb +1 -0
  5. data/lib/dynamoid/adapter.rb +7 -4
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +14 -11
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +1 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +8 -7
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +3 -2
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +1 -0
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +1 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +1 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -0
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -0
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +1 -0
  17. data/lib/dynamoid/application_time_zone.rb +1 -0
  18. data/lib/dynamoid/associations.rb +182 -19
  19. data/lib/dynamoid/associations/association.rb +4 -2
  20. data/lib/dynamoid/associations/belongs_to.rb +2 -1
  21. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
  22. data/lib/dynamoid/associations/has_many.rb +2 -1
  23. data/lib/dynamoid/associations/has_one.rb +2 -1
  24. data/lib/dynamoid/associations/many_association.rb +65 -22
  25. data/lib/dynamoid/associations/single_association.rb +28 -1
  26. data/lib/dynamoid/components.rb +1 -0
  27. data/lib/dynamoid/config.rb +3 -2
  28. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
  29. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
  30. data/lib/dynamoid/config/options.rb +1 -0
  31. data/lib/dynamoid/criteria.rb +1 -0
  32. data/lib/dynamoid/criteria/chain.rb +353 -33
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +1 -0
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +10 -1
  35. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +1 -0
  36. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -0
  37. data/lib/dynamoid/dirty.rb +71 -16
  38. data/lib/dynamoid/document.rb +123 -42
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/fields.rb +189 -16
  42. data/lib/dynamoid/finders.rb +65 -28
  43. data/lib/dynamoid/identity_map.rb +6 -0
  44. data/lib/dynamoid/indexes.rb +74 -15
  45. data/lib/dynamoid/log/formatter.rb +26 -0
  46. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  47. data/lib/dynamoid/persistence.rb +452 -106
  48. data/lib/dynamoid/persistence/import.rb +1 -0
  49. data/lib/dynamoid/persistence/save.rb +1 -0
  50. data/lib/dynamoid/persistence/update_fields.rb +1 -0
  51. data/lib/dynamoid/persistence/upsert.rb +1 -0
  52. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  53. data/lib/dynamoid/railtie.rb +1 -0
  54. data/lib/dynamoid/tasks/database.rb +1 -0
  55. data/lib/dynamoid/type_casting.rb +12 -0
  56. data/lib/dynamoid/undumping.rb +8 -0
  57. data/lib/dynamoid/validations.rb +2 -0
  58. data/lib/dynamoid/version.rb +1 -1
  59. metadata +7 -19
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # The base association module which all associations include. Every association has two very important components: the source and
5
5
  # the target. The source is the object which is calling the association information. It always has the target_ids inside of an attribute on itself.
6
6
  # The target is the object which is referencing by this association.
7
+ # @private
7
8
  module Associations
9
+ # @private
8
10
  module Association
9
11
  attr_accessor :name, :options, :source, :loaded
10
12
 
@@ -116,7 +118,7 @@ module Dynamoid #:nodoc:
116
118
 
117
119
  # Create a new instance of the target class without trying to add it to the association. This creates a base, that caller can update before setting or adding it.
118
120
  #
119
- # @param [Hash] attribute hash for the new object
121
+ # @param attributes [Hash] attribute values for the new object
120
122
  #
121
123
  # @return [Dynamoid::Document] the newly-created object
122
124
  #
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # The belongs_to association. For belongs_to, we reference only a single target instead of multiple records; that target is the
5
5
  # object to which the association object is associated.
6
6
  module Associations
7
+ # @private
7
8
  class BelongsTo
8
9
  include SingleAssociation
9
10
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # The has and belongs to many association.
5
5
  module Associations
6
+ # @private
6
7
  class HasAndBelongsToMany
7
8
  include ManyAssociation
8
9
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # The has_many association.
5
5
  module Associations
6
+ # @private
6
7
  class HasMany
7
8
  include ManyAssociation
8
9
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # The HasOne association.
5
5
  module Associations
6
+ # @private
6
7
  class HasOne
7
8
  include Association
8
9
  include SingleAssociation
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  module Associations
5
5
  module ManyAssociation
6
6
  include Association
@@ -13,6 +13,8 @@ module Dynamoid #:nodoc:
13
13
  end
14
14
 
15
15
  include Enumerable
16
+
17
+ # @private
16
18
  # Delegate methods to the records the association represents.
17
19
  delegate :first, :last, :empty?, :size, :class, to: :records
18
20
 
@@ -20,11 +22,13 @@ module Dynamoid #:nodoc:
20
22
  #
21
23
  # @return the association records; depending on which association this is, either a single instance or an array
22
24
  #
25
+ # @private
23
26
  # @since 0.2.0
24
27
  def find_target
25
28
  Array(target_class.find(source_ids.to_a))
26
29
  end
27
30
 
31
+ # @private
28
32
  def records
29
33
  if query.empty?
30
34
  target
@@ -43,13 +47,20 @@ module Dynamoid #:nodoc:
43
47
  records.include?(object)
44
48
  end
45
49
 
46
- # Deletes an object or array of objects from the association. This removes their records from the association field on the source,
47
- # and attempts to remove the source from the target association if it is detected to exist.
50
+ # Delete an object or array of objects from the association.
51
+ #
52
+ # tag.posts.delete(post)
53
+ # tag.posts.delete([post1, post2, post3])
48
54
  #
49
- # @param [Dynamoid::Document] object the object (or array of objects) to remove from the association
55
+ # This removes their records from the association field on the source,
56
+ # and attempts to remove the source from the target association if it is
57
+ # detected to exist.
50
58
  #
51
- # @return [Dynamoid::Document] the deleted object
59
+ # It saves both models immediately - the source model and the target one
60
+ # so any not saved changes will be saved as well.
52
61
  #
62
+ # @param object [Dynamoid::Document|Array] model (or array of models) to remove from the association
63
+ # @return [Dynamoid::Document|Array] the deleted model
53
64
  # @since 0.2.0
54
65
  def delete(object)
55
66
  disassociate(Array(object).collect(&:hash_key))
@@ -59,13 +70,19 @@ module Dynamoid #:nodoc:
59
70
  object
60
71
  end
61
72
 
62
- # Add an object or array of objects to an association. This preserves the current records in the association (if any)
63
- # and adds the object to the target association if it is detected to exist.
73
+ # Add an object or array of objects to an association.
64
74
  #
65
- # @param [Dynamoid::Document] object the object (or array of objects) to add to the association
75
+ # tag.posts << post
76
+ # tag.posts << [post1, post2, post3]
77
+ #
78
+ # This preserves the current records in the association (if any) and adds
79
+ # the object to the target association if it is detected to exist.
66
80
  #
67
- # @return [Dynamoid::Document] the added object
81
+ # It saves both models immediately - the source model and the target one
82
+ # so any not saved changes will be saved as well.
68
83
  #
84
+ # @param object [Dynamoid::Document|Array] model (or array of models) to add to the association
85
+ # @return [Dynamoid::Document] the added model
69
86
  # @since 0.2.0
70
87
  def <<(object)
71
88
  associate(Array(object).collect(&:hash_key))
@@ -82,8 +99,9 @@ module Dynamoid #:nodoc:
82
99
  #
83
100
  # @param [Dynamoid::Document] object the object (or array of objects) to add to the association
84
101
  #
85
- # @return [Dynamoid::Document] the added object
102
+ # @return [Dynamoid::Document|Array] the added object
86
103
  #
104
+ # @private
87
105
  # @since 0.2.0
88
106
  def setter(object)
89
107
  target.each { |o| delete(o) }
@@ -91,23 +109,37 @@ module Dynamoid #:nodoc:
91
109
  object
92
110
  end
93
111
 
94
- # Create a new instance of the target class and add it directly to the association. If the create fails an exception will be raised.
112
+ # Create a new instance of the target class, persist it and add directly
113
+ # to the association.
95
114
  #
96
- # @param [Hash] attribute hash for the new object
115
+ # tag.posts.create!(title: 'foo')
97
116
  #
98
- # @return [Dynamoid::Document] the newly-created object
117
+ # Several models can be created at once when an array of attributes
118
+ # specified:
119
+ #
120
+ # tag.posts.create!([{ title: 'foo' }, {title: 'bar'} ])
121
+ #
122
+ # If the creation fails an exception will be raised.
99
123
  #
124
+ # @param attributes [Hash] attribute values for the new object
125
+ # @return [Dynamoid::Document|Array] the newly-created object
100
126
  # @since 0.2.0
101
127
  def create!(attributes = {})
102
128
  self << target_class.create!(attributes)
103
129
  end
104
130
 
105
- # Create a new instance of the target class and add it directly to the association.
131
+ # Create a new instance of the target class, persist it and add directly
132
+ # to the association.
106
133
  #
107
- # @param [Hash] attribute hash for the new object
134
+ # tag.posts.create(title: 'foo')
108
135
  #
109
- # @return [Dynamoid::Document] the newly-created object
136
+ # Several models can be created at once when an array of attributes
137
+ # specified:
138
+ #
139
+ # tag.posts.create([{ title: 'foo' }, {title: 'bar'} ])
110
140
  #
141
+ # @param attributes [Hash] attribute values for the new object
142
+ # @return [Dynamoid::Document|Array] the newly-created object
111
143
  # @since 0.2.0
112
144
  def create(attributes = {})
113
145
  self << target_class.create(attributes)
@@ -115,16 +147,18 @@ module Dynamoid #:nodoc:
115
147
 
116
148
  # Create a new instance of the target class and add it directly to the association. If the create fails an exception will be raised.
117
149
  #
118
- # @param [Hash] attribute hash for the new object
119
- #
120
150
  # @return [Dynamoid::Document] the newly-created object
121
151
  #
152
+ # @private
122
153
  # @since 0.2.0
123
154
  def each(&block)
124
155
  records.each(&block)
125
156
  end
126
157
 
127
- # Destroys all members of the association and removes them from the association.
158
+ # Destroys all members of the association and removes them from the
159
+ # association.
160
+ #
161
+ # tag.posts.destroy_all
128
162
  #
129
163
  # @since 0.2.0
130
164
  def destroy_all
@@ -133,7 +167,10 @@ module Dynamoid #:nodoc:
133
167
  objs.each(&:destroy)
134
168
  end
135
169
 
136
- # Deletes all members of the association and removes them from the association.
170
+ # Deletes all members of the association and removes them from the
171
+ # association.
172
+ #
173
+ # tag.posts.delete_all
137
174
  #
138
175
  # @since 0.2.0
139
176
  def delete_all
@@ -144,10 +181,13 @@ module Dynamoid #:nodoc:
144
181
 
145
182
  # Naive association filtering.
146
183
  #
147
- # @param [Hash] A hash of attributes; each must match every returned object's attribute exactly.
184
+ # tag.posts.where(title: 'foo')
148
185
  #
149
- # @return [Dynamoid::Association] the association this method was called on (for chaining purposes)
186
+ # It loads lazily all the associated models and checks provided
187
+ # conditions. That's why only equality conditions can be specified.
150
188
  #
189
+ # @param args [Hash] A hash of attributes; each must match every returned object's attribute exactly.
190
+ # @return [Dynamoid::Association] the association this method was called on (for chaining purposes)
151
191
  # @since 0.2.0
152
192
  def where(args)
153
193
  filtered = clone
@@ -167,6 +207,7 @@ module Dynamoid #:nodoc:
167
207
 
168
208
  # Delegate methods we don't find directly to the records array.
169
209
  #
210
+ # @private
170
211
  # @since 0.2.0
171
212
  def method_missing(method, *args)
172
213
  if records.respond_to?(method)
@@ -176,10 +217,12 @@ module Dynamoid #:nodoc:
176
217
  end
177
218
  end
178
219
 
220
+ # @private
179
221
  def associate(hash_key)
180
222
  source.update_attribute(source_attribute, source_ids.merge(Array(hash_key)))
181
223
  end
182
224
 
225
+ # @private
183
226
  def disassociate(hash_key)
184
227
  source.update_attribute(source_attribute, source_ids - Array(hash_key))
185
228
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  module Associations
5
5
  module SingleAssociation
6
6
  include Association
7
7
 
8
8
  delegate :class, to: :target
9
9
 
10
+ # @private
10
11
  def setter(object)
11
12
  if object.nil?
12
13
  delete
@@ -19,16 +20,37 @@ module Dynamoid #:nodoc:
19
20
  object
20
21
  end
21
22
 
23
+ # Delete a model from the association.
24
+ #
25
+ # post.logo.delete # => nil
26
+ #
27
+ # Saves both models immediately - a source model and a target one so any
28
+ # unsaved changes will be saved. Doesn't delete an associated model from
29
+ # DynamoDB.
22
30
  def delete
23
31
  target.send(target_association).disassociate(source.hash_key) if target && target_association
24
32
  disassociate
25
33
  target
26
34
  end
27
35
 
36
+ # Create a new instance of the target class, persist it and associate.
37
+ #
38
+ # post.logo.create!(hight: 50, width: 90)
39
+ #
40
+ # If the creation fails an exception will be raised.
41
+ #
42
+ # @param attributes [Hash] attributes of a model to create
43
+ # @return [Dynamoid::Document] created model
28
44
  def create!(attributes = {})
29
45
  setter(target_class.create!(attributes))
30
46
  end
31
47
 
48
+ # Create a new instance of the target class, persist it and associate.
49
+ #
50
+ # post.logo.create(hight: 50, width: 90)
51
+ #
52
+ # @param attributes [Hash] attributes of a model to create
53
+ # @return [Dynamoid::Document] created model
32
54
  def create(attributes = {})
33
55
  setter(target_class.create(attributes))
34
56
  end
@@ -44,6 +66,7 @@ module Dynamoid #:nodoc:
44
66
 
45
67
  # Delegate methods we don't find directly to the target.
46
68
  #
69
+ # @private
47
70
  # @since 0.2.0
48
71
  def method_missing(method, *args)
49
72
  if target.respond_to?(method)
@@ -53,21 +76,25 @@ module Dynamoid #:nodoc:
53
76
  end
54
77
  end
55
78
 
79
+ # @private
56
80
  def nil?
57
81
  target.nil?
58
82
  end
59
83
 
84
+ # @private
60
85
  def empty?
61
86
  # This is needed to that ActiveSupport's #blank? and #present?
62
87
  # methods work as expected for SingleAssociations.
63
88
  target.nil?
64
89
  end
65
90
 
91
+ # @private
66
92
  def associate(hash_key)
67
93
  target.send(target_association).disassociate(source.hash_key) if target && target_association
68
94
  source.update_attribute(source_attribute, Set[hash_key])
69
95
  end
70
96
 
97
+ # @private
71
98
  def disassociate(_hash_key = nil)
72
99
  source.update_attribute(source_attribute, nil)
73
100
  end
@@ -3,6 +3,7 @@
3
3
  module Dynamoid
4
4
  # All modules that a Document is composed of are defined in this
5
5
  # module, to keep the document class from getting too cluttered.
6
+ # @private
6
7
  module Components
7
8
  extend ActiveSupport::Concern
8
9
 
@@ -2,13 +2,13 @@
2
2
 
3
3
  require 'uri'
4
4
  require 'logger'
5
- require 'null_logger'
6
5
  require 'dynamoid/config/options'
7
6
  require 'dynamoid/config/backoff_strategies/constant_backoff'
8
7
  require 'dynamoid/config/backoff_strategies/exponential_backoff'
9
8
 
10
9
  module Dynamoid
11
10
  # Contains all the basic configuration information required for Dynamoid: both sensible defaults and required fields.
11
+ # @private
12
12
  module Config
13
13
  # @since 3.3.1
14
14
  DEFAULT_NAMESPACE = if defined?(Rails)
@@ -54,6 +54,7 @@ module Dynamoid
54
54
  constant: BackoffStrategies::ConstantBackoff,
55
55
  exponential: BackoffStrategies::ExponentialBackoff
56
56
  }
57
+ option :log_formatter, default: nil
57
58
  option :http_continue_timeout, default: nil # specify if you'd like to overwrite Aws Configure - default: 1
58
59
  option :http_idle_timeout, default: nil # - default: 5
59
60
  option :http_open_timeout, default: nil # - default: 15
@@ -78,7 +79,7 @@ module Dynamoid
78
79
  # @since 0.2.0
79
80
  def logger=(logger)
80
81
  case logger
81
- when false, nil then @logger = NullLogger.new
82
+ when false, nil then @logger = ::Logger.new(nil)
82
83
  when true then @logger = default_logger
83
84
  else
84
85
  @logger = logger if logger.respond_to?(:info)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Dynamoid
4
4
  module Config
5
+ # @private
5
6
  module BackoffStrategies
6
7
  class ConstantBackoff
7
8
  def self.call(sec = 1)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Dynamoid
4
4
  module Config
5
+ # @private
5
6
  module BackoffStrategies
6
7
  # Truncated binary exponential backoff algorithm
7
8
  # See https://en.wikipedia.org/wiki/Exponential_backoff
@@ -4,6 +4,7 @@
4
4
  module Dynamoid
5
5
  module Config
6
6
  # Encapsulates logic for setting options.
7
+ # @private
7
8
  module Options
8
9
  # Get the defaults or initialize a new empty hash.
9
10
  #
@@ -7,6 +7,7 @@ module Dynamoid
7
7
  module Criteria
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ # @private
10
11
  module ClassMethods
11
12
  %i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages project pluck].each do |meth|
12
13
  # Return a criteria chain in response to a method that will begin or end a chain. For more information,
@@ -22,26 +22,74 @@ module Dynamoid
22
22
  @consistent_read = false
23
23
  @scan_index_forward = true
24
24
 
25
- # Honor STI and :type field if it presents
26
- type = @source.inheritance_field
27
- if @source.attributes.key?(type)
28
- @query[:"#{type}.in"] = @source.deep_subclasses.map(&:name) << @source.name
29
- end
30
-
31
25
  # we should re-initialize keys detector every time we change query
32
26
  @key_fields_detector = KeyFieldsDetector.new(@query, @source)
33
27
  end
34
28
 
35
- # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
36
- # ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
37
- # an attribute name with a range operator.
29
+ # Returns a chain which is a result of filtering current chain with the specified conditions.
30
+ #
31
+ # It accepts conditions in the form of a hash.
32
+ #
33
+ # Post.where(links_count: 2)
34
+ #
35
+ # A key could be either string or symbol.
36
+ #
37
+ # In order to express conditions other than equality predicates could be used.
38
+ # Predicate should be added to an attribute name to form a key +'created_at.gt' => Date.yesterday+
39
+ #
40
+ # Currently supported following predicates:
41
+ # - +gt+ - greater than
42
+ # - +gte+ - greater or equal
43
+ # - +lt+ - less than
44
+ # - +lte+ - less or equal
45
+ # - +ne+ - not equal
46
+ # - +between+ - an attribute value is greater than the first value and less than the second value
47
+ # - +in+ - check an attribute in a list of values
48
+ # - +begins_with+ - check for a prefix in string
49
+ # - +contains+ - check substring or value in a set or array
50
+ # - +not_contains+ - check for absence of substring or a value in set or array
51
+ # - +null+ - attribute doesn't exists in an item
52
+ # - +not_null+ - attribute exists in an item
53
+ #
54
+ # All the predicates match operators supported by DynamoDB's
55
+ # {ComparisonOperator}[https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html#DDB-Type-Condition-ComparisonOperator]
56
+ #
57
+ # Post.where('size.gt' => 1000)
58
+ # Post.where('size.gte' => 1000)
59
+ # Post.where('size.lt' => 35000)
60
+ # Post.where('size.lte' => 35000)
61
+ # Post.where('author.ne' => 'John Doe')
62
+ # Post.where('created_at.between' => [Time.now - 3600, Time.now])
63
+ # Post.where('category.in' => ['tech', 'fashion'])
64
+ # Post.where('title.begins_with' => 'How long')
65
+ # Post.where('tags.contains' => 'Ruby')
66
+ # Post.where('tags.not_contains' => 'Ruby on Rails')
67
+ # Post.where('legacy_attribute.null' => true)
68
+ # Post.where('optional_attribute.not_null' => true)
69
+ #
70
+ # There are some limitations for a sort key. Only following predicates
71
+ # are supported - +gt+, +gte+, +lt+, +lte+, +between+, +begins_with+.
72
+ #
73
+ # +where+ without argument will return the current chain.
74
+ #
75
+ # Multiple calls can be chained together and conditions will be merged:
76
+ #
77
+ # Post.where('size.gt' => 1000).where('title' => 'some title')
38
78
  #
39
- # @example A simple criteria
40
- # where(:name => 'Josh')
79
+ # It's equivalent to:
41
80
  #
42
- # @example A more complicated criteria
43
- # where(:name => 'Josh', 'created_at.gt' => DateTime.now - 1.day)
81
+ # Post.where('size.gt' => 1000, 'title' => 'some title')
44
82
  #
83
+ # But only one condition can be specified for a certain attribute. The
84
+ # last specified condition will override all the others. Only condition
85
+ # 'size.lt' => 200 will be used in following examples:
86
+ #
87
+ # Post.where('size.gt' => 100, 'size.lt' => 200)
88
+ # Post.where('size.gt' => 100).where('size.lt' => 200)
89
+ #
90
+ # Internally +where+ performs either +Scan+ or +Query+ operation.
91
+ #
92
+ # @return [Dynamoid::Criteria::Chain]
45
93
  # @since 0.2.0
46
94
  def where(args)
47
95
  detector = IgnoredConditionsDetector.new(args)
@@ -67,6 +115,13 @@ module Dynamoid
67
115
  self
68
116
  end
69
117
 
118
+ # Turns on strongly consistent reads.
119
+ #
120
+ # By default reads are eventually consistent.
121
+ #
122
+ # Post.where('size.gt' => 1000).consistent
123
+ #
124
+ # @return [Dynamoid::Criteria::Chain]
70
125
  def consistent
71
126
  @consistent_read = true
72
127
  self
@@ -74,11 +129,39 @@ module Dynamoid
74
129
 
75
130
  # Returns all the records matching the criteria.
76
131
  #
132
+ # Since +where+ and most of the other methods return a +Chain+
133
+ # the only way to get a result as a collection is to call the +all+
134
+ # method. It returns +Enumerator+ which could be used directly or
135
+ # transformed into +Array+
136
+ #
137
+ # Post.all # => Enumerator
138
+ # Post.where(links_count: 2).all # => Enumerator
139
+ # Post.where(links_count: 2).all.to_a # => Array
140
+ #
141
+ # When the result set is too large DynamoDB divides it into separate
142
+ # pages. While an enumerator iterates over the result models each page
143
+ # is loaded lazily. So even an extra large result set can be loaded and
144
+ # processed with considerably small memory footprint and throughput
145
+ # consumption.
146
+ #
147
+ # @return [Enumerator::Lazy]
77
148
  # @since 0.2.0
78
149
  def all
79
150
  records
80
151
  end
81
152
 
153
+ # Returns the actual number of items in a table matching the criteria.
154
+ #
155
+ # Post.where(links_count: 2).count
156
+ #
157
+ # Internally it uses either `Scan` or `Query` DynamoDB's operation so it
158
+ # costs like all the matching items were read from a table.
159
+ #
160
+ # The only difference is that items are read by DynemoDB but not actually
161
+ # loaded on the client side. DynamoDB returns only count of items after
162
+ # filtering.
163
+ #
164
+ # @return [Integer]
82
165
  def count
83
166
  if @key_fields_detector.key_present?
84
167
  count_via_query
@@ -87,16 +170,61 @@ module Dynamoid
87
170
  end
88
171
  end
89
172
 
90
- # Returns the last fetched record matched the criteria
91
- # Enumerable doesn't implement `last`, only `first`
92
- # So we have to implement it ourselves
173
+ # Returns the first item matching the criteria.
174
+ #
175
+ # Post.where(links_count: 2).first
176
+ #
177
+ # Applies `record_limit(1)` to ensure only a single record is fetched
178
+ # when no non-key conditions are present and `scan_limit(1)` when no
179
+ # conditions are present at all.
180
+ #
181
+ # If used without criteria it just returns the first item of some
182
+ # arbitrary order.
183
+ #
184
+ # Post.first
185
+ #
186
+ # @return [Model|nil]
187
+ def first(*args)
188
+ n = args.first || 1
189
+
190
+ return scan_limit(n).to_a.first(*args) if @query.blank?
191
+ return super if @key_fields_detector.non_key_present?
192
+
193
+ record_limit(n).to_a.first(*args)
194
+ end
195
+
196
+ # Returns the last item matching the criteria.
197
+ #
198
+ # Post.where(links_count: 2).last
93
199
  #
200
+ # DynamoDB doesn't support ordering by some arbitrary attribute except a
201
+ # sort key. So this method is mostly useful during development and
202
+ # testing.
203
+ #
204
+ # If used without criteria it just returns the last item of some arbitrary order.
205
+ #
206
+ # Post.last
207
+ #
208
+ # It isn't efficient from the performance point of view as far as it reads and
209
+ # loads all the filtered items from DynamoDB.
210
+ #
211
+ # @return [Model|nil]
94
212
  def last
95
213
  all.to_a.last
96
214
  end
97
215
 
98
- # Destroys all the records matching the criteria.
216
+ # Deletes all the items matching the criteria.
99
217
  #
218
+ # Post.where(links_count: 2).delete_all
219
+ #
220
+ # If called without criteria then it deletes all the items in a table.
221
+ #
222
+ # Post.delete_all
223
+ #
224
+ # It loads all the items either with +Scan+ or +Query+ operation and
225
+ # deletes them in batch with +BatchWriteItem+ operation. +BatchWriteItem+
226
+ # is limited by request size and items count so it's quite possible the
227
+ # deletion will require several +BatchWriteItem+ calls.
100
228
  def delete_all
101
229
  ids = []
102
230
  ranges = []
@@ -117,53 +245,215 @@ module Dynamoid
117
245
  end
118
246
  alias destroy_all delete_all
119
247
 
120
- # The record limit is the limit of evaluated records returned by the
121
- # query or scan.
248
+ # Set the record limit.
249
+ #
250
+ # The record limit is the limit of evaluated items returned by the
251
+ # +Query+ or +Scan+. In other words it's how many items should be
252
+ # returned in response.
253
+ #
254
+ # Post.where(links_count: 2).record_limit(1000) # => 1000 models
255
+ # Post.record_limit(1000) # => 1000 models
256
+ #
257
+ # It could be very inefficient in terms of HTTP requests in pathological
258
+ # cases. DynamoDB doesn't support out of the box the limits for items
259
+ # count after filtering. So it's possible to make a lot of HTTP requests
260
+ # to find items matching criteria and skip not matching. It means that
261
+ # the cost (read capacity units) is unpredictable.
262
+ #
263
+ # Because of such issues with performance and cost it's mostly useful in
264
+ # development and testing.
265
+ #
266
+ # When called without criteria it works like +scan_limit+.
267
+ #
268
+ # @return [Dynamoid::Criteria::Chain]
122
269
  def record_limit(limit)
123
270
  @record_limit = limit
124
271
  self
125
272
  end
126
273
 
127
- # The scan limit which is the limit of records that DynamoDB will
128
- # internally query or scan. This is different from the record limit
129
- # as with filtering DynamoDB may look at N scanned records but return 0
130
- # records if none pass the filter.
274
+ # Set the scan limit.
275
+ #
276
+ # The scan limit is the limit of records that DynamoDB will internally
277
+ # read with +Query+ or +Scan+. It's different from the record limit as
278
+ # with filtering DynamoDB may look at N scanned items but return 0
279
+ # items if none passes the filter. So it can return less items than was
280
+ # specified with the limit.
281
+ #
282
+ # Post.where(links_count: 2).scan_limit(1000) # => 850 models
283
+ # Post.scan_limit(1000) # => 1000 models
284
+ #
285
+ # By contrast with +record_limit+ the cost (read capacity units) and
286
+ # performance is predictable.
287
+ #
288
+ # When called without criteria it works like +record_limit+.
289
+ #
290
+ # @return [Dynamoid::Criteria::Chain]
131
291
  def scan_limit(limit)
132
292
  @scan_limit = limit
133
293
  self
134
294
  end
135
295
 
296
+ # Set the batch size.
297
+ #
298
+ # The batch size is a number of items which will be lazily loaded one by one.
299
+ # When the batch size is set then items will be loaded batch by batch of
300
+ # the specified size instead of relying on the default paging mechanism
301
+ # of DynamoDB.
302
+ #
303
+ # Post.where(links_count: 2).batch(1000).all.each do |post|
304
+ # # process a post
305
+ # end
306
+ #
307
+ # It's useful to limit memory usage or throughput consumption
308
+ #
309
+ # @return [Dynamoid::Criteria::Chain]
136
310
  def batch(batch_size)
137
311
  @batch_size = batch_size
138
312
  self
139
313
  end
140
314
 
315
+ # Set the start item.
316
+ #
317
+ # When the start item is set the items will be loaded starting right
318
+ # after the specified item.
319
+ #
320
+ # Post.where(links_count: 2).start(post)
321
+ #
322
+ # It can be used to implement an own pagination mechanism.
323
+ #
324
+ # Post.where(author_id: author_id).start(last_post).scan_limit(50)
325
+ #
326
+ # The specified start item will not be returned back in a result set.
327
+ #
328
+ # Actually it doesn't need all the item attributes to start - an item may
329
+ # have only the primary key attributes (partition and sort key if it's
330
+ # declared).
331
+ #
332
+ # Post.where(links_count: 2).start(Post.new(id: id))
333
+ #
334
+ # It also supports a +Hash+ argument with the keys attributes - a
335
+ # partition key and a sort key (if it's declared).
336
+ #
337
+ # Post.where(links_count: 2).start(id: id)
338
+ #
339
+ # @return [Dynamoid::Criteria::Chain]
141
340
  def start(start)
142
341
  @start = start
143
342
  self
144
343
  end
145
344
 
345
+ # Reverse the sort order.
346
+ #
347
+ # By default the sort order is ascending (by the sort key value). Set a
348
+ # +false+ value to reverse the order.
349
+ #
350
+ # Post.where(id: id, 'views_count.gt' => 1000).scan_index_forward(false)
351
+ #
352
+ # It works only for queries with a partition key condition e.g. +id:
353
+ # 'some-id'+ which internally performs +Query+ operation.
354
+ #
355
+ # @return [Dynamoid::Criteria::Chain]
146
356
  def scan_index_forward(scan_index_forward)
147
357
  @scan_index_forward = scan_index_forward
148
358
  self
149
359
  end
150
360
 
151
- # Allows you to use the results of a search as an enumerable over the results found.
361
+ # Allows to use the results of a search as an enumerable over the results
362
+ # found.
363
+ #
364
+ # Post.each do |post|
365
+ # end
366
+ #
367
+ # Post.all.each do |post|
368
+ # end
369
+ #
370
+ # Post.where(links_count: 2).each do |post|
371
+ # end
372
+ #
373
+ # It works similar to the +all+ method so results are loaded lazily.
152
374
  #
153
375
  # @since 0.2.0
154
376
  def each(&block)
155
377
  records.each(&block)
156
378
  end
157
379
 
380
+ # Iterates over the pages returned by DynamoDB.
381
+ #
382
+ # DynamoDB has its own paging machanism and divides a large result set
383
+ # into separate pages. The +find_by_pages+ method provides access to
384
+ # these native DynamoDB pages.
385
+ #
386
+ # The pages are loaded lazily.
387
+ #
388
+ # Post.where('views_count.gt' => 1000).find_by_pages do |posts, options|
389
+ # # process posts
390
+ # end
391
+ #
392
+ # It passes as block argument an +Array+ of models and a Hash with options.
393
+ #
394
+ # Options +Hash+ contains only one option +:last_evaluated_key+. The last
395
+ # evaluated key is a Hash with key attributes of the last item processed by
396
+ # DynamoDB. It can be used to resume querying using the +start+ method.
397
+ #
398
+ # posts, options = Post.where('views_count.gt' => 1000).find_by_pages.first
399
+ # last_key = options[:last_evaluated_key]
400
+ #
401
+ # # ...
402
+ #
403
+ # Post.where('views_count.gt' => 1000).start(last_key).find_by_pages do |posts, options|
404
+ # end
405
+ #
406
+ # If it's called without a block then it returns an +Enumerator+.
407
+ #
408
+ # enum = Post.where('views_count.gt' => 1000).find_by_pages
409
+ #
410
+ # enum.each do |posts, options|
411
+ # # process posts
412
+ # end
413
+ #
414
+ # @return [Enumerator::Lazy]
158
415
  def find_by_pages(&block)
159
416
  pages.each(&block)
160
417
  end
161
418
 
419
+ # Select only specified fields.
420
+ #
421
+ # It takes one or more field names and returns a collection of models with only
422
+ # these fields set.
423
+ #
424
+ # Post.where('views_count.gt' => 1000).select(:title)
425
+ # Post.where('views_count.gt' => 1000).select(:title, :created_at)
426
+ # Post.select(:id)
427
+ #
428
+ # It can be used to avoid loading large field values and to decrease a
429
+ # memory footprint.
430
+ #
431
+ # @return [Dynamoid::Criteria::Chain]
162
432
  def project(*fields)
163
433
  @project = fields.map(&:to_sym)
164
434
  self
165
435
  end
166
436
 
437
+ # Select only specified fields.
438
+ #
439
+ # It takes one or more field names and returns an array of either values
440
+ # or arrays of values.
441
+ #
442
+ # Post.pluck(:id) # => ['1', '2']
443
+ # Post.pluck(:title, :title) # => [['1', 'Title #1'], ['2', 'Title#2']]
444
+ #
445
+ # Post.where('views_count.gt' => 1000).pluck(:title)
446
+ #
447
+ # There are some differences between +pluck+ and +project+. +pluck+
448
+ # - doesn't instantiate models
449
+ # - it isn't chainable and returns +Array+ instead of +Chain+
450
+ #
451
+ # It deserializes values if a field type isn't supported by DynamoDB natively.
452
+ #
453
+ # It can be used to avoid loading large field values and to decrease a
454
+ # memory footprint.
455
+ #
456
+ # @return [Array]
167
457
  def pluck(*args)
168
458
  fields = args.map(&:to_sym)
169
459
  @project = fields
@@ -325,6 +615,13 @@ module Dynamoid
325
615
 
326
616
  def range_query
327
617
  opts = {}
618
+ query = self.query
619
+
620
+ # Honor STI and :type field if it presents
621
+ if @source.attributes.key?(@source.inheritance_field) &&
622
+ @key_fields_detector.hash_key.to_sym != @source.inheritance_field.to_sym
623
+ query.update(sti_condition)
624
+ end
328
625
 
329
626
  # Add hash key
330
627
  opts[:hash_key] = @key_fields_detector.hash_key
@@ -332,15 +629,7 @@ module Dynamoid
332
629
 
333
630
  # Add range key
334
631
  if @key_fields_detector.range_key
335
- opts[:range_key] = @key_fields_detector.range_key
336
- if query[@key_fields_detector.range_key].present?
337
- value = type_cast_condition_parameter(@key_fields_detector.range_key, query[@key_fields_detector.range_key])
338
- opts.update(range_eq: value)
339
- end
340
-
341
- query.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
342
- opts.merge!(range_hash(key))
343
- end
632
+ add_range_key_to_range_query(query, opts)
344
633
  end
345
634
 
346
635
  (query.keys.map(&:to_sym) - [@key_fields_detector.hash_key.to_sym, @key_fields_detector.range_key.try(:to_sym)])
@@ -357,6 +646,18 @@ module Dynamoid
357
646
  opts.merge(query_opts).merge(consistent_opts)
358
647
  end
359
648
 
649
+ def add_range_key_to_range_query(query, opts)
650
+ opts[:range_key] = @key_fields_detector.range_key
651
+ if query[@key_fields_detector.range_key].present?
652
+ value = type_cast_condition_parameter(@key_fields_detector.range_key, query[@key_fields_detector.range_key])
653
+ opts.update(range_eq: value)
654
+ end
655
+
656
+ query.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
657
+ opts.merge!(range_hash(key))
658
+ end
659
+ end
660
+
360
661
  # TODO: casting should be operator aware
361
662
  # e.g. for NULL operator value should be boolean
362
663
  # and isn't related to an attribute own type
@@ -418,6 +719,13 @@ module Dynamoid
418
719
  end
419
720
 
420
721
  def scan_query
722
+ query = self.query
723
+
724
+ # Honor STI and :type field if it presents
725
+ if sti_condition
726
+ query.update(sti_condition)
727
+ end
728
+
421
729
  {}.tap do |opts|
422
730
  query.keys.map(&:to_sym).each do |key|
423
731
  if key.to_s.include?('.')
@@ -440,6 +748,18 @@ module Dynamoid
440
748
  opts[:project] = @project
441
749
  opts
442
750
  end
751
+
752
+ def sti_condition
753
+ condition = {}
754
+ type = @source.inheritance_field
755
+
756
+ if @source.attributes.key?(type)
757
+ class_names = @source.deep_subclasses.map(&:name) << @source.name
758
+ condition[:"#{type}.in"] = class_names
759
+ end
760
+
761
+ condition
762
+ end
443
763
  end
444
764
  end
445
765
  end