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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -4
- data/README.md +24 -18
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +7 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +14 -11
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +8 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +3 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +1 -0
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +4 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +65 -22
- data/lib/dynamoid/associations/single_association.rb +28 -1
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +3 -2
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +1 -0
- data/lib/dynamoid/criteria/chain.rb +353 -33
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +1 -0
- data/lib/dynamoid/criteria/key_fields_detector.rb +10 -1
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +1 -0
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -0
- data/lib/dynamoid/dirty.rb +71 -16
- data/lib/dynamoid/document.rb +123 -42
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/fields.rb +189 -16
- data/lib/dynamoid/finders.rb +65 -28
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +74 -15
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +452 -106
- data/lib/dynamoid/persistence/import.rb +1 -0
- data/lib/dynamoid/persistence/save.rb +1 -0
- data/lib/dynamoid/persistence/update_fields.rb +1 -0
- data/lib/dynamoid/persistence/upsert.rb +1 -0
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -0
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +7 -19
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Dynamoid
|
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
|
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
|
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,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Dynamoid
|
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
|
-
#
|
47
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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.
|
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
|
-
#
|
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
|
-
#
|
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
|
112
|
+
# Create a new instance of the target class, persist it and add directly
|
113
|
+
# to the association.
|
95
114
|
#
|
96
|
-
#
|
115
|
+
# tag.posts.create!(title: 'foo')
|
97
116
|
#
|
98
|
-
#
|
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
|
131
|
+
# Create a new instance of the target class, persist it and add directly
|
132
|
+
# to the association.
|
106
133
|
#
|
107
|
-
#
|
134
|
+
# tag.posts.create(title: 'foo')
|
108
135
|
#
|
109
|
-
#
|
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
|
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
|
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
|
-
#
|
184
|
+
# tag.posts.where(title: 'foo')
|
148
185
|
#
|
149
|
-
#
|
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
|
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
|
data/lib/dynamoid/components.rb
CHANGED
data/lib/dynamoid/config.rb
CHANGED
@@ -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 =
|
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)
|
data/lib/dynamoid/criteria.rb
CHANGED
@@ -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
|
-
#
|
36
|
-
#
|
37
|
-
#
|
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
|
-
#
|
40
|
-
# where(:name => 'Josh')
|
79
|
+
# It's equivalent to:
|
41
80
|
#
|
42
|
-
#
|
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
|
91
|
-
#
|
92
|
-
#
|
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
|
-
#
|
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
|
-
#
|
121
|
-
#
|
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
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
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
|
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
|
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
|