dynamoid 0.4.1 → 0.5.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.
@@ -97,16 +97,7 @@ module Dynamoid #:nodoc:
97
97
  #
98
98
  # @since 0.2.0
99
99
  def records_with_index
100
- ids = if index.range_key?
101
- Dynamoid::Adapter.query(index.table_name, index_query).collect{|r| r[:ids]}.inject(Set.new) {|set, result| set + result}
102
- else
103
- results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
104
- if results
105
- results[:ids]
106
- else
107
- []
108
- end
109
- end
100
+ ids = ids_from_index
110
101
  if ids.nil? || ids.empty?
111
102
  []
112
103
  else
@@ -121,8 +112,22 @@ module Dynamoid #:nodoc:
121
112
  end
122
113
  end
123
114
 
115
+ # Returns the Set of IDs from the index table.
116
+ #
117
+ # @return [Set] a Set containing the IDs from the index.
118
+ def ids_from_index
119
+ if index.range_key?
120
+ Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts)).inject(Set.new) do |all, record|
121
+ all + Set.new(record[:ids])
122
+ end
123
+ else
124
+ results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
125
+ results ? results[:ids] : []
126
+ end
127
+ end
128
+
124
129
  def records_with_range
125
- Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
130
+ Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.from_database(hash) }
126
131
  end
127
132
 
128
133
  # If the query does not match an index, we'll manually scan the associated table to find results.
@@ -136,7 +141,11 @@ module Dynamoid #:nodoc:
136
141
  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(', ')}]"
137
142
  end
138
143
 
139
- Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
144
+ if @consistent_read
145
+ raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
146
+ end
147
+
148
+ Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.from_database(hash) }
140
149
  end
141
150
 
142
151
  # Format the provided query so that it can be used to query results from DynamoDB.
@@ -201,7 +210,7 @@ module Dynamoid #:nodoc:
201
210
 
202
211
  def range?
203
212
  return false unless source.range_key
204
- query_keys == ['id'] || (query_keys.to_set == ['id', source.range_key.to_s].to_set)
213
+ query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
205
214
  end
206
215
 
207
216
  def start_key
@@ -0,0 +1,41 @@
1
+ module Dynamoid
2
+ module Dirty
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Dirty
5
+
6
+ module ClassMethods
7
+ def from_database(*)
8
+ super.tap { |d| d.changed_attributes.clear }
9
+ end
10
+ end
11
+
12
+ def save(*)
13
+ clear_changes { super }
14
+ end
15
+
16
+ def reload
17
+ super.tap { clear_changes }
18
+ end
19
+
20
+ def clear_changes
21
+ previous = changes
22
+ (block_given? ? yield : true).tap do |result|
23
+ unless result == false #failed validation; nil is OK.
24
+ @previously_changed = previous
25
+ changed_attributes.clear
26
+ end
27
+ end
28
+ end
29
+
30
+ def write_attribute(name, value)
31
+ attribute_will_change!(name) unless self.read_attribute(name) == value
32
+ super
33
+ end
34
+
35
+ protected
36
+
37
+ def attribute_method?(attr)
38
+ super || self.class.attributes.has_key?(attr.to_sym)
39
+ end
40
+ end
41
+ end
@@ -6,18 +6,18 @@ module Dynamoid #:nodoc:
6
6
  module Document
7
7
  extend ActiveSupport::Concern
8
8
  include Dynamoid::Components
9
-
9
+
10
10
  included do
11
11
  class_attribute :options
12
12
  self.options = {}
13
-
13
+
14
14
  Dynamoid::Config.included_models << self
15
15
  end
16
-
16
+
17
17
  module ClassMethods
18
- # Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
18
+ # Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
19
19
  # write capacity.
20
- #
20
+ #
21
21
  # @param [Hash] options options to pass for this table
22
22
  # @option options [Symbol] :name the name for the table; this still gets namespaced
23
23
  # @option options [Symbol] :id id column for the table
@@ -28,21 +28,21 @@ module Dynamoid #:nodoc:
28
28
  def table(options = {})
29
29
  self.options = options
30
30
  end
31
-
31
+
32
32
  # Returns the read_capacity for this table.
33
33
  #
34
34
  # @since 0.4.0
35
35
  def read_capacity
36
36
  options[:read_capacity] || Dynamoid::Config.read_capacity
37
37
  end
38
-
38
+
39
39
  # Returns the write_capacity for this table.
40
40
  #
41
41
  # @since 0.4.0
42
42
  def write_capacity
43
43
  options[:write_capacity] || Dynamoid::Config.write_capacity
44
44
  end
45
-
45
+
46
46
  # Returns the id field for this class.
47
47
  #
48
48
  # @since 0.4.0
@@ -71,7 +71,7 @@ module Dynamoid #:nodoc:
71
71
  def create!(attrs = {})
72
72
  new(attrs).tap(&:save!)
73
73
  end
74
-
74
+
75
75
  # Initialize a new object.
76
76
  #
77
77
  # @param [Hash] attrs Attributes with which to create the object.
@@ -101,50 +101,79 @@ module Dynamoid #:nodoc:
101
101
  #
102
102
  # @return [Dynamoid::Document] the new document
103
103
  #
104
- # @since 0.2.0
104
+ # @since 0.2.0
105
105
  def initialize(attrs = {})
106
- self.class.send(:field, self.class.hash_key) unless self.respond_to?(self.class.hash_key)
107
-
108
- @new_record = true
109
- @attributes ||= {}
110
- @associations ||= {}
106
+ run_callbacks :initialize do
107
+ self.class.send(:field, self.class.hash_key) unless self.respond_to?(self.class.hash_key)
108
+
109
+ @new_record = true
110
+ @attributes ||= {}
111
+ @associations ||= {}
112
+
113
+ load(attrs)
114
+ end
115
+ end
111
116
 
117
+ def load(attrs)
112
118
  self.class.undump(attrs).each {|key, value| send "#{key}=", value }
113
119
  end
114
120
 
115
121
  # An object is equal to another object if their ids are equal.
116
122
  #
117
- # @since 0.2.0
123
+ # @since 0.2.0
118
124
  def ==(other)
119
- return false if other.nil?
120
- other.respond_to?(:hash_key) && other.hash_key == self.hash_key
125
+ if self.class.identity_map_on?
126
+ super
127
+ else
128
+ return false if other.nil?
129
+ other.respond_to?(:hash_key) && other.hash_key == self.hash_key
130
+ end
121
131
  end
122
132
 
123
- # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
133
+ # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
124
134
  # changes to be reflected immediately, you would call this method.
125
135
  #
126
136
  # @return [Dynamoid::Document] the document this method was called on
127
137
  #
128
- # @since 0.2.0
138
+ # @since 0.2.0
129
139
  def reload
130
- self.attributes = self.class.find(self.hash_key).attributes
140
+ self.attributes = self.class.find(hash_key, :range_key => range_value).attributes
131
141
  @associations.values.each(&:reset)
132
142
  self
133
143
  end
134
-
144
+
135
145
  # Return an object's hash key, regardless of what it might be called to the object.
136
146
  #
137
147
  # @since 0.4.0
138
148
  def hash_key
139
149
  self.send(self.class.hash_key)
140
150
  end
141
-
151
+
142
152
  # Assign an object's hash key, regardless of what it might be called to the object.
143
153
  #
144
154
  # @since 0.4.0
145
- def hash_key=(key)
146
- self.send("#{self.class.hash_key}=".to_sym, key)
155
+ def hash_key=(value)
156
+ self.send("#{self.class.hash_key}=", value)
157
+ end
158
+
159
+ def range_value
160
+ if range_key = self.class.range_key
161
+ self.send(range_key)
162
+ end
163
+ end
164
+
165
+ def range_value=(value)
166
+ self.send("#{self.class.range_key}=", value)
167
+ end
168
+
169
+ def range_value
170
+ if range_key = self.class.range_key
171
+ self.send(range_key)
172
+ end
173
+ end
174
+
175
+ def range_value=(value)
176
+ self.send("#{self.class.range_key}=", value)
147
177
  end
148
178
  end
149
-
150
179
  end
@@ -13,11 +13,16 @@ module Dynamoid
13
13
  # MissingRangeKey is raised when a table that requires a range key is quieried without one.
14
14
  class MissingRangeKey < Error; end
15
15
 
16
+ # raised when the conditional check failed during update operation
17
+ class ConditionalCheckFailedException < Error; end
18
+
16
19
  # DocumentNotValid is raised when the document fails validation.
17
20
  class DocumentNotValid < Error
18
21
  def initialize(document)
19
22
  super("Validation failed: #{document.errors.full_messages.join(", ")}")
20
23
  end
21
24
  end
25
+
26
+ class InvalidQuery < Error; end
22
27
  end
23
28
  end
@@ -12,7 +12,6 @@ module Dynamoid #:nodoc:
12
12
  class_attribute :range_key
13
13
 
14
14
  self.attributes = {}
15
-
16
15
  field :created_at, :datetime
17
16
  field :updated_at, :datetime
18
17
  end
@@ -34,8 +33,6 @@ module Dynamoid #:nodoc:
34
33
  define_method(named) { read_attribute(named) }
35
34
  define_method("#{named}?") { !read_attribute(named).nil? }
36
35
  define_method("#{named}=") {|value| write_attribute(named, value) }
37
-
38
- respond_to?(:define_attribute_method) ? define_attribute_method(name) : define_attribute_methods([name])
39
36
  end
40
37
 
41
38
  def range(name, type = :string)
@@ -59,8 +56,6 @@ module Dynamoid #:nodoc:
59
56
  Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
60
57
  end
61
58
 
62
- attribute_will_change!(name) unless self.read_attribute(name) == value
63
-
64
59
  if association = @associations[name]
65
60
  association.reset
66
61
  end
@@ -5,9 +5,9 @@ module Dynamoid
5
5
  # class level, like find, find_by_id, and the method_missing style finders.
6
6
  module Finders
7
7
  extend ActiveSupport::Concern
8
-
8
+
9
9
  module ClassMethods
10
-
10
+
11
11
  # Find one or many objects, specified by one id or an array of ids.
12
12
  #
13
13
  # @param [Array/String] *id an array of ids or one single id
@@ -27,25 +27,73 @@ module Dynamoid
27
27
  if ids.count == 1
28
28
  self.find_by_id(ids.first, options)
29
29
  else
30
- items = Dynamoid::Adapter.read(self.table_name, ids, options)
31
- items[self.table_name].collect{|i| self.build(i).tap { |o| o.new_record = false } }
30
+ find_all(ids)
32
31
  end
33
32
  end
34
33
 
34
+ # Find all object by hash key or hash and range key
35
+ #
36
+ # @param [Array<ID>] ids
37
+ #
38
+ # @example
39
+ # find all the user with hash key
40
+ # User.find_all(['1', '2', '3'])
41
+ #
42
+ # find all the tweets using hash key and range key
43
+ # Tweet.find_all([['1', 'red'], ['1', 'green'])
44
+ def find_all(ids)
45
+ items = Dynamoid::Adapter.read(self.table_name, ids, options)
46
+ items[self.table_name].collect{|i| from_database(i) }
47
+ end
48
+
35
49
  # Find one object directly by id.
36
50
  #
37
51
  # @param [String] id the id of the object to find
38
52
  #
39
53
  # @return [Dynamoid::Document] the found object, or nil if nothing was found
40
54
  #
41
- # @since 0.2.0
55
+ # @since 0.2.0
42
56
  def find_by_id(id, options = {})
43
57
  if item = Dynamoid::Adapter.read(self.table_name, id, options)
44
- obj = self.new(item)
45
- obj.new_record = false
46
- return obj
58
+ from_database(item)
47
59
  else
48
- return nil
60
+ nil
61
+ end
62
+ end
63
+
64
+ # Find one object directly by hash and range keys
65
+ #
66
+ # @param [String] hash_key of the object to find
67
+ # @param [String/Integer/Float] range_key of the object to find
68
+ #
69
+ def find_by_composite_key(hash_key, range_key, options = {})
70
+ find_by_id(hash_key, options.merge({:range_key => range_key}))
71
+ end
72
+
73
+ # Find all objects by hash and range keys.
74
+ #
75
+ # @example find all ChamberTypes whose level is greater than 1
76
+ # class ChamberType
77
+ # include Dynamoid::Document
78
+ # field :chamber_type, :string
79
+ # range :level, :integer
80
+ # table :key => :chamber_type
81
+ # end
82
+ # ChamberType.find_all_by_composite_key('DustVault', range_greater_than: 1)
83
+ #
84
+ # @param [String] hash_key of the objects to find
85
+ # @param [Hash] options the options for the range key
86
+ # @option options [Range] :range_value find the range key within this range
87
+ # @option options [Number] :range_greater_than find range keys greater than this
88
+ # @option options [Number] :range_less_than find range keys less than this
89
+ # @option options [Number] :range_gte find range keys greater than or equal to this
90
+ # @option options [Number] :range_lte find range keys less than or equal to this
91
+ #
92
+ # @return [Array] an array of all matching items
93
+ #
94
+ def find_all_by_composite_key(hash_key, options = {})
95
+ Dynamoid::Adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item|
96
+ from_database(item)
49
97
  end
50
98
  end
51
99
 
@@ -59,7 +107,7 @@ module Dynamoid
59
107
  #
60
108
  # @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
61
109
  #
62
- # @since 0.2.0
110
+ # @since 0.2.0
63
111
  def method_missing(method, *args)
64
112
  if method =~ /find/
65
113
  finder = method.to_s.split('_by_').first
@@ -67,7 +115,7 @@ module Dynamoid
67
115
 
68
116
  chain = Dynamoid::Criteria::Chain.new(self)
69
117
  chain.query = Hash.new.tap {|h| attributes.each_with_index {|attr, index| h[attr.to_sym] = args[index]}}
70
-
118
+
71
119
  if finder =~ /all/
72
120
  return chain.all
73
121
  else
@@ -79,5 +127,5 @@ module Dynamoid
79
127
  end
80
128
  end
81
129
  end
82
-
130
+
83
131
  end
@@ -0,0 +1,96 @@
1
+ module Dynamoid
2
+ module IdentityMap
3
+ extend ActiveSupport::Concern
4
+
5
+ def self.clear
6
+ models.each { |m| m.identity_map.clear }
7
+ end
8
+
9
+ def self.models
10
+ Dynamoid::Config.included_models
11
+ end
12
+
13
+ module ClassMethods
14
+ def identity_map
15
+ @identity_map ||= {}
16
+ end
17
+
18
+ def from_database(attrs = {})
19
+ return super if identity_map_off?
20
+
21
+ key = identity_map_key(attrs)
22
+ document = identity_map[key]
23
+
24
+ if document.nil?
25
+ document = super
26
+ identity_map[key] = document
27
+ else
28
+ document.load(attrs)
29
+ end
30
+
31
+ document
32
+ end
33
+
34
+ def find_by_id(id, options = {})
35
+ return super if identity_map_off?
36
+
37
+ key = id.to_s
38
+
39
+ if range_key = options[:range_key]
40
+ key += "::#{range_key}"
41
+ end
42
+
43
+ if identity_map[key]
44
+ identity_map[key]
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def identity_map_key(attrs)
51
+ key = attrs[hash_key].to_s
52
+ if range_key
53
+ key += "::#{attrs[range_key]}"
54
+ end
55
+ key
56
+ end
57
+
58
+ def identity_map_on?
59
+ Dynamoid::Config.identity_map
60
+ end
61
+
62
+ def identity_map_off?
63
+ !identity_map_on?
64
+ end
65
+ end
66
+
67
+ def identity_map
68
+ self.class.identity_map
69
+ end
70
+
71
+ def save(*args)
72
+ return super if self.class.identity_map_off?
73
+
74
+ if result = super
75
+ identity_map[identity_map_key] = self
76
+ end
77
+ result
78
+ end
79
+
80
+ def delete
81
+ return super if self.class.identity_map_off?
82
+
83
+ identity_map.delete(identity_map_key)
84
+ super
85
+ end
86
+
87
+
88
+ def identity_map_key
89
+ key = hash_key.to_s
90
+ if self.class.range_key
91
+ key += "::#{range_value}"
92
+ end
93
+ key
94
+ end
95
+ end
96
+ end