dynamoid 0.3.2 → 0.4.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.
- data/Dynamoid.gemspec +3 -2
- data/README.markdown +39 -3
- data/VERSION +1 -1
- data/lib/dynamoid.rb +2 -0
- data/lib/dynamoid/adapter.rb +9 -8
- data/lib/dynamoid/adapter/aws_sdk.rb +15 -9
- data/lib/dynamoid/adapter/local.rb +39 -14
- data/lib/dynamoid/associations.rb +5 -6
- data/lib/dynamoid/associations/association.rb +23 -1
- data/lib/dynamoid/associations/belongs_to.rb +0 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -1
- data/lib/dynamoid/associations/has_many.rb +0 -1
- data/lib/dynamoid/associations/many_association.rb +10 -7
- data/lib/dynamoid/associations/single_association.rb +2 -1
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria.rb +2 -2
- data/lib/dynamoid/criteria/chain.rb +118 -43
- data/lib/dynamoid/document.rb +58 -6
- data/lib/dynamoid/fields.rb +18 -3
- data/lib/dynamoid/finders.rb +14 -7
- data/lib/dynamoid/indexes.rb +3 -2
- data/lib/dynamoid/indexes/index.rb +2 -2
- data/lib/dynamoid/persistence.rb +29 -14
- data/spec/app/models/address.rb +4 -0
- data/spec/app/models/camel_case.rb +13 -0
- data/spec/app/models/tweet.rb +9 -0
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +5 -5
- data/spec/dynamoid/adapter/local_spec.rb +79 -79
- data/spec/dynamoid/adapter_spec.rb +6 -6
- data/spec/dynamoid/associations/association_spec.rb +26 -12
- data/spec/dynamoid/criteria/chain_spec.rb +64 -21
- data/spec/dynamoid/criteria_spec.rb +28 -0
- data/spec/dynamoid/document_spec.rb +29 -0
- data/spec/dynamoid/fields_spec.rb +5 -0
- data/spec/dynamoid/finders_spec.rb +6 -1
- data/spec/dynamoid/indexes/index_spec.rb +1 -1
- data/spec/dynamoid/persistence_spec.rb +9 -17
- data/spec/spec_helper.rb +1 -0
- metadata +4 -3
@@ -3,6 +3,7 @@ module Dynamoid #:nodoc:
|
|
3
3
|
|
4
4
|
module Associations
|
5
5
|
module SingleAssociation
|
6
|
+
include Association
|
6
7
|
|
7
8
|
delegate :class, :to => :target
|
8
9
|
|
@@ -59,7 +60,7 @@ module Dynamoid #:nodoc:
|
|
59
60
|
# @return [Dynamoid::Document] the found target (or nil if nothing)
|
60
61
|
#
|
61
62
|
# @since 0.2.0
|
62
|
-
def
|
63
|
+
def find_target
|
63
64
|
return if source_ids.empty?
|
64
65
|
target_class.find(source_ids.first)
|
65
66
|
end
|
data/lib/dynamoid/components.rb
CHANGED
data/lib/dynamoid/config.rb
CHANGED
@@ -21,6 +21,7 @@ module Dynamoid
|
|
21
21
|
option :warn_on_scan, :default => true
|
22
22
|
option :partitioning, :default => false
|
23
23
|
option :partition_size, :default => 200
|
24
|
+
option :endpoint, :default => 'dynamodb.us-east-1.amazonaws.com'
|
24
25
|
option :included_models, :default => []
|
25
26
|
|
26
27
|
# The default logger for Dynamoid: either the Rails logger or just stdout.
|
data/lib/dynamoid/criteria.rb
CHANGED
@@ -9,7 +9,7 @@ module Dynamoid
|
|
9
9
|
|
10
10
|
module ClassMethods
|
11
11
|
|
12
|
-
[:where, :all, :first, :each].each do |meth|
|
12
|
+
[:where, :all, :first, :each, :limit, :start].each do |meth|
|
13
13
|
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
14
14
|
# see Dynamoid::Criteria::Chain.
|
15
15
|
#
|
@@ -26,4 +26,4 @@ module Dynamoid
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
end
|
29
|
+
end
|
@@ -6,20 +6,21 @@ module Dynamoid #:nodoc:
|
|
6
6
|
# chain to relation). It is a chainable object that builds up a query and eventually executes it either on an index
|
7
7
|
# or by a full table scan.
|
8
8
|
class Chain
|
9
|
-
attr_accessor :query, :source, :index, :values
|
9
|
+
attr_accessor :query, :source, :index, :values, :limit, :start, :consistent_read
|
10
10
|
include Enumerable
|
11
|
-
|
11
|
+
|
12
12
|
# Create a new criteria chain.
|
13
13
|
#
|
14
14
|
# @param [Class] source the class upon which the ultimate query will be performed.
|
15
15
|
def initialize(source)
|
16
16
|
@query = {}
|
17
17
|
@source = source
|
18
|
+
@consistent_read = false
|
18
19
|
end
|
19
|
-
|
20
|
-
# The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
|
21
|
-
# ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
|
22
|
-
# an attribute name with a range operator.
|
20
|
+
|
21
|
+
# The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
|
22
|
+
# ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
|
23
|
+
# an attribute name with a range operator.
|
23
24
|
#
|
24
25
|
# @example A simple criteria
|
25
26
|
# where(:name => 'Josh')
|
@@ -32,7 +33,12 @@ module Dynamoid #:nodoc:
|
|
32
33
|
args.each {|k, v| query[k] = v}
|
33
34
|
self
|
34
35
|
end
|
35
|
-
|
36
|
+
|
37
|
+
def consistent
|
38
|
+
@consistent_read = true
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
36
42
|
# Returns all the records matching the criteria.
|
37
43
|
#
|
38
44
|
# @since 0.2.0
|
@@ -42,40 +48,59 @@ module Dynamoid #:nodoc:
|
|
42
48
|
|
43
49
|
# Returns the first record matching the criteria.
|
44
50
|
#
|
45
|
-
# @since 0.2.0
|
51
|
+
# @since 0.2.0
|
46
52
|
def first
|
47
|
-
|
53
|
+
limit(1).first
|
54
|
+
end
|
55
|
+
|
56
|
+
def limit(limit)
|
57
|
+
@limit = limit
|
58
|
+
records
|
59
|
+
end
|
60
|
+
|
61
|
+
def start(start)
|
62
|
+
@start = start
|
63
|
+
self
|
48
64
|
end
|
49
65
|
|
50
66
|
# Allows you to use the results of a search as an enumerable over the results found.
|
51
67
|
#
|
52
|
-
# @since 0.2.0
|
68
|
+
# @since 0.2.0
|
53
69
|
def each(&block)
|
54
70
|
records.each(&block)
|
55
71
|
end
|
56
|
-
|
72
|
+
|
73
|
+
def consistent_opts
|
74
|
+
{ :consistent_read => consistent_read }
|
75
|
+
end
|
76
|
+
|
57
77
|
private
|
58
|
-
|
78
|
+
|
59
79
|
# The actual records referenced by the association.
|
60
80
|
#
|
61
81
|
# @return [Array] an array of the found records.
|
62
82
|
#
|
63
83
|
# @since 0.2.0
|
64
84
|
def records
|
65
|
-
|
66
|
-
|
85
|
+
if range?
|
86
|
+
records_with_range
|
87
|
+
elsif index
|
88
|
+
records_with_index
|
89
|
+
else
|
90
|
+
records_without_index
|
91
|
+
end
|
67
92
|
end
|
68
93
|
|
69
94
|
# If the query matches an index on the associated class, then this method will retrieve results from the index table.
|
70
95
|
#
|
71
96
|
# @return [Array] an array of the found records.
|
72
97
|
#
|
73
|
-
# @since 0.2.0
|
98
|
+
# @since 0.2.0
|
74
99
|
def records_with_index
|
75
100
|
ids = if index.range_key?
|
76
101
|
Dynamoid::Adapter.query(index.table_name, index_query).collect{|r| r[:ids]}.inject(Set.new) {|set, result| set + result}
|
77
102
|
else
|
78
|
-
results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value])
|
103
|
+
results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
|
79
104
|
if results
|
80
105
|
results[:ids]
|
81
106
|
else
|
@@ -85,28 +110,40 @@ module Dynamoid #:nodoc:
|
|
85
110
|
if ids.nil? || ids.empty?
|
86
111
|
[]
|
87
112
|
else
|
88
|
-
|
113
|
+
ids = ids.to_a
|
114
|
+
|
115
|
+
if @start
|
116
|
+
ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
|
117
|
+
end
|
118
|
+
|
119
|
+
ids = ids.take(@limit) if @limit
|
120
|
+
Array(source.find(ids, consistent_opts))
|
89
121
|
end
|
90
122
|
end
|
91
|
-
|
92
|
-
|
123
|
+
|
124
|
+
def records_with_range
|
125
|
+
Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
|
126
|
+
end
|
127
|
+
|
128
|
+
# If the query does not match an index, we'll manually scan the associated table to find results.
|
93
129
|
#
|
94
130
|
# @return [Array] an array of the found records.
|
95
131
|
#
|
96
|
-
# @since 0.2.0
|
132
|
+
# @since 0.2.0
|
97
133
|
def records_without_index
|
98
134
|
if Dynamoid::Config.warn_on_scan
|
99
135
|
Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
|
100
136
|
Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{source.attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
|
101
137
|
end
|
102
|
-
|
138
|
+
|
139
|
+
Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
|
103
140
|
end
|
104
|
-
|
105
|
-
# Format the provided query so that it can be used to query results from DynamoDB.
|
141
|
+
|
142
|
+
# Format the provided query so that it can be used to query results from DynamoDB.
|
106
143
|
#
|
107
144
|
# @return [Hash] a hash with keys of :hash_value and :range_value
|
108
145
|
#
|
109
|
-
# @since 0.2.0
|
146
|
+
# @since 0.2.0
|
110
147
|
def index_query
|
111
148
|
values = index.values(query)
|
112
149
|
{}.tap do |hash|
|
@@ -114,21 +151,7 @@ module Dynamoid #:nodoc:
|
|
114
151
|
if index.range_key?
|
115
152
|
key = query.keys.find{|k| k.to_s.include?('.')}
|
116
153
|
if key
|
117
|
-
|
118
|
-
hash[:range_value] = query[key]
|
119
|
-
else
|
120
|
-
val = query[key].to_f
|
121
|
-
case key.split('.').last
|
122
|
-
when 'gt'
|
123
|
-
hash[:range_greater_than] = val
|
124
|
-
when 'lt'
|
125
|
-
hash[:range_less_than] = val
|
126
|
-
when 'gte'
|
127
|
-
hash[:range_gte] = val
|
128
|
-
when 'lte'
|
129
|
-
hash[:range_lte] = val
|
130
|
-
end
|
131
|
-
end
|
154
|
+
hash.merge!(range_hash(key))
|
132
155
|
else
|
133
156
|
raise Dynamoid::Errors::MissingRangeKey, 'This index requires a range key'
|
134
157
|
end
|
@@ -136,16 +159,68 @@ module Dynamoid #:nodoc:
|
|
136
159
|
end
|
137
160
|
end
|
138
161
|
|
162
|
+
def range_hash(key)
|
163
|
+
val = query[key]
|
164
|
+
|
165
|
+
return { :range_value => query[key] } if query[key].is_a?(Range)
|
166
|
+
|
167
|
+
case key.split('.').last
|
168
|
+
when 'gt'
|
169
|
+
{ :range_greater_than => val.to_f }
|
170
|
+
when 'lt'
|
171
|
+
{ :range_less_than => val.to_f }
|
172
|
+
when 'gte'
|
173
|
+
{ :range_gte => val.to_f }
|
174
|
+
when 'lte'
|
175
|
+
{ :range_lte => val.to_f }
|
176
|
+
when 'begins_with'
|
177
|
+
{ :range_begins_with => val }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def range_query
|
182
|
+
opts = { :hash_value => query[source.hash_key] }
|
183
|
+
if key = query.keys.find { |k| k.to_s.include?('.') }
|
184
|
+
opts.merge!(range_key(key))
|
185
|
+
end
|
186
|
+
opts.merge(query_opts).merge(consistent_opts)
|
187
|
+
end
|
188
|
+
|
139
189
|
# Return an index that fulfills all the attributes the criteria is querying, or nil if none is found.
|
140
190
|
#
|
141
|
-
# @since 0.2.0
|
191
|
+
# @since 0.2.0
|
142
192
|
def index
|
143
|
-
index = source.find_index(
|
193
|
+
index = source.find_index(query_keys)
|
144
194
|
return nil if index.blank?
|
145
195
|
index
|
146
196
|
end
|
197
|
+
|
198
|
+
def query_keys
|
199
|
+
query.keys.collect{|k| k.to_s.split('.').first}
|
200
|
+
end
|
201
|
+
|
202
|
+
def range?
|
203
|
+
return false unless source.range_key
|
204
|
+
query_keys == ['id'] || (query_keys.to_set == ['id', source.range_key.to_s].to_set)
|
205
|
+
end
|
206
|
+
|
207
|
+
def start_key
|
208
|
+
key = { :hash_key_element => { 'S' => @start.hash_key } }
|
209
|
+
if range_key = @start.class.range_key
|
210
|
+
range_key_type = @start.class.attributes[range_key][:type] == :string ? 'S' : 'N'
|
211
|
+
key.merge!({:range_key_element => { range_key_type => @start.send(range_key) } })
|
212
|
+
end
|
213
|
+
key
|
214
|
+
end
|
215
|
+
|
216
|
+
def query_opts
|
217
|
+
opts = {}
|
218
|
+
opts[:limit] = @limit if @limit
|
219
|
+
opts[:next_token] = start_key if @start
|
220
|
+
opts
|
221
|
+
end
|
147
222
|
end
|
148
|
-
|
223
|
+
|
149
224
|
end
|
150
|
-
|
225
|
+
|
151
226
|
end
|
data/lib/dynamoid/document.rb
CHANGED
@@ -8,10 +8,47 @@ module Dynamoid #:nodoc:
|
|
8
8
|
include Dynamoid::Components
|
9
9
|
|
10
10
|
included do
|
11
|
+
class_attribute :options
|
12
|
+
self.options = {}
|
13
|
+
|
11
14
|
Dynamoid::Config.included_models << self
|
12
15
|
end
|
13
16
|
|
14
17
|
module ClassMethods
|
18
|
+
# Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
|
19
|
+
# write capacity.
|
20
|
+
#
|
21
|
+
# @param [Hash] options options to pass for this table
|
22
|
+
# @option options [Symbol] :name the name for the table; this still gets namespaced
|
23
|
+
# @option options [Symbol] :id id column for the table
|
24
|
+
# @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
|
25
|
+
# @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
|
26
|
+
#
|
27
|
+
# @since 0.4.0
|
28
|
+
def table(options = {})
|
29
|
+
self.options = options
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the read_capacity for this table.
|
33
|
+
#
|
34
|
+
# @since 0.4.0
|
35
|
+
def read_capacity
|
36
|
+
options[:read_capacity] || Dynamoid::Config.read_capacity
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the write_capacity for this table.
|
40
|
+
#
|
41
|
+
# @since 0.4.0
|
42
|
+
def write_capacity
|
43
|
+
options[:write_capacity] || Dynamoid::Config.write_capacity
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the id field for this class.
|
47
|
+
#
|
48
|
+
# @since 0.4.0
|
49
|
+
def hash_key
|
50
|
+
options[:key] || :id
|
51
|
+
end
|
15
52
|
|
16
53
|
# Initialize a new object and immediately save it to the database.
|
17
54
|
#
|
@@ -66,13 +103,13 @@ module Dynamoid #:nodoc:
|
|
66
103
|
#
|
67
104
|
# @since 0.2.0
|
68
105
|
def initialize(attrs = {})
|
106
|
+
self.class.send(:field, self.class.hash_key) unless self.respond_to?(self.class.hash_key)
|
107
|
+
|
69
108
|
@new_record = true
|
70
109
|
@attributes ||= {}
|
71
|
-
|
110
|
+
@associations ||= {}
|
72
111
|
|
73
|
-
self.class.
|
74
|
-
send "#{attribute}=", incoming_attributes[attribute]
|
75
|
-
end
|
112
|
+
self.class.undump(attrs).each {|key, value| send "#{key}=", value }
|
76
113
|
end
|
77
114
|
|
78
115
|
# An object is equal to another object if their ids are equal.
|
@@ -80,7 +117,7 @@ module Dynamoid #:nodoc:
|
|
80
117
|
# @since 0.2.0
|
81
118
|
def ==(other)
|
82
119
|
return false if other.nil?
|
83
|
-
other.respond_to?(:
|
120
|
+
other.respond_to?(:hash_key) && other.hash_key == self.hash_key
|
84
121
|
end
|
85
122
|
|
86
123
|
# Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
|
@@ -90,9 +127,24 @@ module Dynamoid #:nodoc:
|
|
90
127
|
#
|
91
128
|
# @since 0.2.0
|
92
129
|
def reload
|
93
|
-
self.attributes = self.class.find(self.
|
130
|
+
self.attributes = self.class.find(self.hash_key).attributes
|
131
|
+
@associations.values.each(&:reset)
|
94
132
|
self
|
95
133
|
end
|
134
|
+
|
135
|
+
# Return an object's hash key, regardless of what it might be called to the object.
|
136
|
+
#
|
137
|
+
# @since 0.4.0
|
138
|
+
def hash_key
|
139
|
+
self.send(self.class.hash_key)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Assign an object's hash key, regardless of what it might be called to the object.
|
143
|
+
#
|
144
|
+
# @since 0.4.0
|
145
|
+
def hash_key=(key)
|
146
|
+
self.send("#{self.class.hash_key}=".to_sym, key)
|
147
|
+
end
|
96
148
|
end
|
97
149
|
|
98
150
|
end
|
data/lib/dynamoid/fields.rb
CHANGED
@@ -9,9 +9,10 @@ module Dynamoid #:nodoc:
|
|
9
9
|
# Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at.
|
10
10
|
included do
|
11
11
|
class_attribute :attributes
|
12
|
-
|
12
|
+
class_attribute :range_key
|
13
|
+
|
13
14
|
self.attributes = {}
|
14
|
-
|
15
|
+
|
15
16
|
field :created_at, :datetime
|
16
17
|
field :updated_at, :datetime
|
17
18
|
end
|
@@ -34,7 +35,12 @@ module Dynamoid #:nodoc:
|
|
34
35
|
define_method("#{named}?") { !read_attribute(named).nil? }
|
35
36
|
define_method("#{named}=") {|value| write_attribute(named, value) }
|
36
37
|
|
37
|
-
define_attribute_method(name)
|
38
|
+
respond_to?(:define_attribute_method) ? define_attribute_method(name) : define_attribute_methods([name])
|
39
|
+
end
|
40
|
+
|
41
|
+
def range(name, type = :string)
|
42
|
+
field(name, type)
|
43
|
+
self.range_key = name
|
38
44
|
end
|
39
45
|
end
|
40
46
|
|
@@ -49,7 +55,16 @@ module Dynamoid #:nodoc:
|
|
49
55
|
#
|
50
56
|
# @since 0.2.0
|
51
57
|
def write_attribute(name, value)
|
58
|
+
if (size = value.to_s.size) > MAX_ITEM_SIZE
|
59
|
+
Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
|
60
|
+
end
|
61
|
+
|
52
62
|
attribute_will_change!(name) unless self.read_attribute(name) == value
|
63
|
+
|
64
|
+
if association = @associations[name]
|
65
|
+
association.reset
|
66
|
+
end
|
67
|
+
|
53
68
|
attributes[name.to_sym] = value
|
54
69
|
end
|
55
70
|
alias :[]= :write_attribute
|
data/lib/dynamoid/finders.rb
CHANGED
@@ -15,12 +15,19 @@ module Dynamoid
|
|
15
15
|
# @return [Dynamoid::Document] one object or an array of objects, depending on whether the input was an array or not
|
16
16
|
#
|
17
17
|
# @since 0.2.0
|
18
|
-
def find(*
|
19
|
-
|
20
|
-
if
|
21
|
-
|
18
|
+
def find(*ids)
|
19
|
+
|
20
|
+
options = if ids.last.is_a? Hash
|
21
|
+
ids.slice!(-1)
|
22
|
+
else
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
|
26
|
+
ids = Array(ids.flatten.uniq)
|
27
|
+
if ids.count == 1
|
28
|
+
self.find_by_id(ids.first, options)
|
22
29
|
else
|
23
|
-
items = Dynamoid::Adapter.read(self.table_name,
|
30
|
+
items = Dynamoid::Adapter.read(self.table_name, ids, options)
|
24
31
|
items[self.table_name].collect{|i| self.build(i).tap { |o| o.new_record = false } }
|
25
32
|
end
|
26
33
|
end
|
@@ -32,8 +39,8 @@ module Dynamoid
|
|
32
39
|
# @return [Dynamoid::Document] the found object, or nil if nothing was found
|
33
40
|
#
|
34
41
|
# @since 0.2.0
|
35
|
-
def find_by_id(id)
|
36
|
-
if item = Dynamoid::Adapter.read(self.table_name, id)
|
42
|
+
def find_by_id(id, options = {})
|
43
|
+
if item = Dynamoid::Adapter.read(self.table_name, id, options)
|
37
44
|
obj = self.new(item)
|
38
45
|
obj.new_record = false
|
39
46
|
return obj
|