dynamoid-moda 0.7.1

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 (136) hide show
  1. checksums.yaml +15 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +24 -0
  6. data/Gemfile.lock +118 -0
  7. data/Gemfile_activemodel4 +24 -0
  8. data/Gemfile_activemodel4.lock +88 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.markdown +360 -0
  11. data/Rakefile +93 -0
  12. data/VERSION +1 -0
  13. data/doc/.nojekyll +0 -0
  14. data/doc/Dynamoid.html +328 -0
  15. data/doc/Dynamoid/Adapter.html +1872 -0
  16. data/doc/Dynamoid/Adapter/AwsSdk.html +2101 -0
  17. data/doc/Dynamoid/Adapter/Local.html +1574 -0
  18. data/doc/Dynamoid/Associations.html +138 -0
  19. data/doc/Dynamoid/Associations/Association.html +847 -0
  20. data/doc/Dynamoid/Associations/BelongsTo.html +161 -0
  21. data/doc/Dynamoid/Associations/ClassMethods.html +766 -0
  22. data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +167 -0
  23. data/doc/Dynamoid/Associations/HasMany.html +167 -0
  24. data/doc/Dynamoid/Associations/HasOne.html +161 -0
  25. data/doc/Dynamoid/Associations/ManyAssociation.html +1684 -0
  26. data/doc/Dynamoid/Associations/SingleAssociation.html +627 -0
  27. data/doc/Dynamoid/Components.html +242 -0
  28. data/doc/Dynamoid/Config.html +412 -0
  29. data/doc/Dynamoid/Config/Options.html +638 -0
  30. data/doc/Dynamoid/Criteria.html +138 -0
  31. data/doc/Dynamoid/Criteria/Chain.html +1471 -0
  32. data/doc/Dynamoid/Criteria/ClassMethods.html +105 -0
  33. data/doc/Dynamoid/Dirty.html +424 -0
  34. data/doc/Dynamoid/Dirty/ClassMethods.html +174 -0
  35. data/doc/Dynamoid/Document.html +1033 -0
  36. data/doc/Dynamoid/Document/ClassMethods.html +1116 -0
  37. data/doc/Dynamoid/Errors.html +125 -0
  38. data/doc/Dynamoid/Errors/ConditionalCheckFailedException.html +141 -0
  39. data/doc/Dynamoid/Errors/DocumentNotValid.html +221 -0
  40. data/doc/Dynamoid/Errors/Error.html +137 -0
  41. data/doc/Dynamoid/Errors/InvalidField.html +141 -0
  42. data/doc/Dynamoid/Errors/InvalidQuery.html +131 -0
  43. data/doc/Dynamoid/Errors/MissingRangeKey.html +141 -0
  44. data/doc/Dynamoid/Fields.html +686 -0
  45. data/doc/Dynamoid/Fields/ClassMethods.html +438 -0
  46. data/doc/Dynamoid/Finders.html +135 -0
  47. data/doc/Dynamoid/Finders/ClassMethods.html +943 -0
  48. data/doc/Dynamoid/IdentityMap.html +492 -0
  49. data/doc/Dynamoid/IdentityMap/ClassMethods.html +534 -0
  50. data/doc/Dynamoid/Indexes.html +321 -0
  51. data/doc/Dynamoid/Indexes/ClassMethods.html +369 -0
  52. data/doc/Dynamoid/Indexes/Index.html +1142 -0
  53. data/doc/Dynamoid/Middleware.html +115 -0
  54. data/doc/Dynamoid/Middleware/IdentityMap.html +264 -0
  55. data/doc/Dynamoid/Persistence.html +892 -0
  56. data/doc/Dynamoid/Persistence/ClassMethods.html +836 -0
  57. data/doc/Dynamoid/Validations.html +415 -0
  58. data/doc/_index.html +506 -0
  59. data/doc/class_list.html +53 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +57 -0
  62. data/doc/css/style.css +338 -0
  63. data/doc/file.LICENSE.html +73 -0
  64. data/doc/file.README.html +416 -0
  65. data/doc/file_list.html +58 -0
  66. data/doc/frames.html +28 -0
  67. data/doc/index.html +416 -0
  68. data/doc/js/app.js +214 -0
  69. data/doc/js/full_list.js +178 -0
  70. data/doc/js/jquery.js +4 -0
  71. data/doc/method_list.html +1144 -0
  72. data/doc/top-level-namespace.html +112 -0
  73. data/dynamoid-moda.gemspec +210 -0
  74. data/dynamoid.gemspec +208 -0
  75. data/lib/dynamoid.rb +46 -0
  76. data/lib/dynamoid/adapter.rb +267 -0
  77. data/lib/dynamoid/adapter/aws_sdk.rb +309 -0
  78. data/lib/dynamoid/associations.rb +106 -0
  79. data/lib/dynamoid/associations/association.rb +105 -0
  80. data/lib/dynamoid/associations/belongs_to.rb +44 -0
  81. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
  82. data/lib/dynamoid/associations/has_many.rb +39 -0
  83. data/lib/dynamoid/associations/has_one.rb +39 -0
  84. data/lib/dynamoid/associations/many_association.rb +191 -0
  85. data/lib/dynamoid/associations/single_association.rb +69 -0
  86. data/lib/dynamoid/components.rb +37 -0
  87. data/lib/dynamoid/config.rb +57 -0
  88. data/lib/dynamoid/config/options.rb +78 -0
  89. data/lib/dynamoid/criteria.rb +29 -0
  90. data/lib/dynamoid/criteria/chain.rb +326 -0
  91. data/lib/dynamoid/dirty.rb +47 -0
  92. data/lib/dynamoid/document.rb +199 -0
  93. data/lib/dynamoid/errors.rb +28 -0
  94. data/lib/dynamoid/fields.rb +138 -0
  95. data/lib/dynamoid/finders.rb +133 -0
  96. data/lib/dynamoid/identity_map.rb +96 -0
  97. data/lib/dynamoid/indexes.rb +69 -0
  98. data/lib/dynamoid/indexes/index.rb +103 -0
  99. data/lib/dynamoid/middleware/identity_map.rb +16 -0
  100. data/lib/dynamoid/persistence.rb +292 -0
  101. data/lib/dynamoid/validations.rb +36 -0
  102. data/spec/app/models/address.rb +13 -0
  103. data/spec/app/models/camel_case.rb +34 -0
  104. data/spec/app/models/car.rb +6 -0
  105. data/spec/app/models/magazine.rb +11 -0
  106. data/spec/app/models/message.rb +9 -0
  107. data/spec/app/models/nuclear_submarine.rb +5 -0
  108. data/spec/app/models/sponsor.rb +8 -0
  109. data/spec/app/models/subscription.rb +12 -0
  110. data/spec/app/models/tweet.rb +12 -0
  111. data/spec/app/models/user.rb +26 -0
  112. data/spec/app/models/vehicle.rb +7 -0
  113. data/spec/dynamoid/adapter/aws_sdk_spec.rb +376 -0
  114. data/spec/dynamoid/adapter_spec.rb +155 -0
  115. data/spec/dynamoid/associations/association_spec.rb +194 -0
  116. data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
  117. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
  118. data/spec/dynamoid/associations/has_many_spec.rb +42 -0
  119. data/spec/dynamoid/associations/has_one_spec.rb +45 -0
  120. data/spec/dynamoid/associations_spec.rb +16 -0
  121. data/spec/dynamoid/config_spec.rb +27 -0
  122. data/spec/dynamoid/criteria/chain_spec.rb +210 -0
  123. data/spec/dynamoid/criteria_spec.rb +75 -0
  124. data/spec/dynamoid/dirty_spec.rb +57 -0
  125. data/spec/dynamoid/document_spec.rb +180 -0
  126. data/spec/dynamoid/fields_spec.rb +156 -0
  127. data/spec/dynamoid/finders_spec.rb +147 -0
  128. data/spec/dynamoid/identity_map_spec.rb +45 -0
  129. data/spec/dynamoid/indexes/index_spec.rb +104 -0
  130. data/spec/dynamoid/indexes_spec.rb +25 -0
  131. data/spec/dynamoid/persistence_spec.rb +301 -0
  132. data/spec/dynamoid/validations_spec.rb +36 -0
  133. data/spec/dynamoid_spec.rb +14 -0
  134. data/spec/spec_helper.rb +55 -0
  135. data/spec/support/with_partitioning.rb +15 -0
  136. metadata +363 -0
@@ -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
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+ require 'dynamoid/indexes/index'
3
+
4
+ module Dynamoid #:nodoc:
5
+
6
+ # Indexes are quick ways of performing queries by anything other than id in DynamoDB. They are denormalized tables;
7
+ # that is, data is duplicated in the initial table (where the object is saved) and the index table (where
8
+ # we perform indexing).
9
+ module Indexes
10
+ extend ActiveSupport::Concern
11
+
12
+ # Make some helpful attributes to persist indexes.
13
+ included do
14
+ class_attribute :indexes
15
+
16
+ self.indexes = {}
17
+ end
18
+
19
+ module ClassMethods
20
+
21
+ # The call to create an index. Generates a new index with the specified options -- for more information, see Dynamoid::Indexes::Index.
22
+ # This function also attempts to immediately create the indexing table if it does not exist already.
23
+ #
24
+ # @since 0.2.0
25
+ def index(name, options = {})
26
+ index = Dynamoid::Indexes::Index.new(self, name, options)
27
+ self.indexes[index.name] = index
28
+ create_indexes
29
+ end
30
+
31
+ # Helper function to find indexes.
32
+ #
33
+ # @since 0.2.0
34
+ def find_index(index)
35
+ self.indexes[Array(index).collect(&:to_s).sort.collect(&:to_sym)]
36
+ end
37
+
38
+ # Helper function to create indexes (if they don't exist already).
39
+ #
40
+ # @since 0.2.0
41
+ def create_indexes
42
+ self.indexes.each do |name, index|
43
+ opts = {:table_name => index.table_name, :id => :id}
44
+ opts[:range_key] = { :range => :number } if index.range_key?
45
+ self.create_table(opts)
46
+ end
47
+ end
48
+ end
49
+
50
+ # Callback for an object to save itself to each of a class' indexes.
51
+ #
52
+ # @since 0.2.0
53
+ def save_indexes
54
+ self.class.indexes.each do |name, index|
55
+ index.save(self)
56
+ end
57
+ end
58
+
59
+ # Callback for an object to delete itself from each of a class' indexes.
60
+ #
61
+ # @since 0.2.0
62
+ def delete_indexes
63
+ self.class.indexes.each do |name, index|
64
+ index.delete(self)
65
+ end
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+ module Indexes
4
+
5
+ # The class contains all the information an index contains, including its keys and which attributes it covers.
6
+ class Index
7
+ attr_accessor :source, :name, :hash_keys, :range_keys
8
+ alias_method :range_key?, :range_keys
9
+
10
+ # Create a new index. Pass either :range => true or :range => :column_name to create a ranged index on that column.
11
+ #
12
+ # @param [Class] source the source class for the index
13
+ # @param [Symbol] name the name of the index
14
+ #
15
+ # @since 0.2.0
16
+ def initialize(source, name, options = {})
17
+ @source = source
18
+
19
+ if options.delete(:range)
20
+ @range_keys = sort(name)
21
+ elsif options[:range_key]
22
+ @range_keys = sort(options[:range_key])
23
+ end
24
+ @hash_keys = sort(name)
25
+ @name = sort([hash_keys, range_keys])
26
+
27
+ raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless keys.all?{|n| source.attributes.include?(n)}
28
+ end
29
+
30
+ # Sort objects into alphabetical strings, used for composing index names correctly (since we always assume they're alphabetical).
31
+ #
32
+ # @example find all users by first and last name
33
+ # sort([:gamma, :alpha, :beta, :omega]) # => [:alpha, :beta, :gamma, :omega]
34
+ #
35
+ # @since 0.2.0
36
+ def sort(objs)
37
+ Array(objs).flatten.compact.uniq.collect(&:to_s).sort.collect(&:to_sym)
38
+ end
39
+
40
+ # Return the array of keys this index uses for its table.
41
+ #
42
+ # @since 0.2.0
43
+ def keys
44
+ [Array(hash_keys) + Array(range_keys)].flatten.uniq
45
+ end
46
+
47
+ # Return the table name for this index.
48
+ #
49
+ # @since 0.2.0
50
+ def table_name
51
+ "#{Dynamoid::Config.namespace}_index_" + source.table_name.sub("#{Dynamoid::Config.namespace}_", '').singularize + "_#{name.collect(&:to_s).collect(&:pluralize).join('_and_')}"
52
+ end
53
+
54
+ # Given either an object or a list of attributes, generate a hash key and a range key for the index. Optionally pass in
55
+ # true to changed_attributes for a list of all the object's dirty attributes in convenient index form (for deleting stale
56
+ # information from the indexes).
57
+ #
58
+ # @param [Object] attrs either an object that responds to :attributes, or a hash of attributes
59
+ #
60
+ # @return [Hash] a hash with the keys :hash_value and :range_value
61
+ #
62
+ # @since 0.2.0
63
+ def values(attrs, changed_attributes = false)
64
+ if changed_attributes
65
+ hash = {}
66
+ attrs.changes.each {|k, v| hash[k.to_sym] = (v.first || v.last)}
67
+ attrs = hash
68
+ end
69
+ attrs = attrs.send(:attributes) if attrs.respond_to?(:attributes)
70
+ {}.tap do |hash|
71
+ hash[:hash_value] = hash_keys.collect{|key| attrs[key]}.join('.')
72
+ hash[:range_value] = range_keys.inject(0.0) {|sum, key| sum + attrs[key].to_f} if self.range_key?
73
+ end
74
+ end
75
+
76
+ # Save an object to this index, merging it with existing ids if there's already something present at this index location.
77
+ # First, though, delete this object from its old indexes (so the object isn't listed in an erroneous index).
78
+ #
79
+ # @since 0.2.0
80
+ def save(obj)
81
+ self.delete(obj, true)
82
+ values = values(obj)
83
+ return true if values[:hash_value].blank? || (!values[:range_value].nil? && values[:range_value].blank?)
84
+ existing = Dynamoid::Adapter.read(self.table_name, values[:hash_value], { :range_key => values[:range_value] })
85
+ ids = ((existing and existing[:ids]) or Set.new)
86
+ Dynamoid::Adapter.write(self.table_name, {:id => values[:hash_value], :ids => ids.merge([obj.id]), :range => values[:range_value]})
87
+ end
88
+
89
+ # Delete an object from this index, preserving existing ids if there are any, and failing gracefully if for some reason the
90
+ # index doesn't already have this object in it.
91
+ #
92
+ # @since 0.2.0
93
+ def delete(obj, changed_attributes = false)
94
+ values = values(obj, changed_attributes)
95
+ return true if values[:hash_value].blank? || (!values[:range_value].nil? && values[:range_value].blank?)
96
+ existing = Dynamoid::Adapter.read(self.table_name, values[:hash_value], { :range_key => values[:range_value]})
97
+ return true unless existing && existing[:ids] && existing[:ids].include?(obj.id)
98
+ Dynamoid::Adapter.write(self.table_name, {:id => values[:hash_value], :ids => (existing[:ids] - Set[obj.id]), :range => values[:range_value]})
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,16 @@
1
+ module Dynamoid
2
+ module Middleware
3
+ class IdentityMap
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ Dynamoid::IdentityMap.clear
10
+ @app.call(env)
11
+ ensure
12
+ Dynamoid::IdentityMap.clear
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,292 @@
1
+ require 'securerandom'
2
+
3
+ # encoding: utf-8
4
+ module Dynamoid
5
+
6
+ # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
7
+ # values to be of the same type as when they were passed in, based on the fields in the class.
8
+ module Persistence
9
+ extend ActiveSupport::Concern
10
+
11
+ attr_accessor :new_record
12
+ alias :new_record? :new_record
13
+
14
+ module ClassMethods
15
+
16
+ def table_name
17
+ @table_name ||= "#{Dynamoid::Config.namespace}_#{options[:name] || base_class.name.split('::').last.downcase.pluralize}"
18
+ end
19
+
20
+ # Creates a table.
21
+ #
22
+ # @param [Hash] options options to pass for table creation
23
+ # @option options [Symbol] :id the id field for the table
24
+ # @option options [Symbol] :table_name the actual name for the table
25
+ # @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
26
+ # @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
27
+ # @option options [Hash] {range_key => :type} a hash of the name of the range key and a symbol of its type
28
+ #
29
+ # @since 0.4.0
30
+ def create_table(options = {})
31
+ if self.range_key
32
+ range_key_hash = { range_key => dynamo_type(attributes[range_key][:type]) }
33
+ else
34
+ range_key_hash = nil
35
+ end
36
+ options = {
37
+ :id => self.hash_key,
38
+ :table_name => self.table_name,
39
+ :write_capacity => self.write_capacity,
40
+ :read_capacity => self.read_capacity,
41
+ :range_key => range_key_hash
42
+ }.merge(options)
43
+
44
+ return true if table_exists?(options[:table_name])
45
+
46
+ Dynamoid::Adapter.tables << options[:table_name] if Dynamoid::Adapter.create_table(options[:table_name], options[:id], options)
47
+ end
48
+
49
+ # Does a table with this name exist?
50
+ #
51
+ # @since 0.2.0
52
+ def table_exists?(table_name)
53
+ Dynamoid::Adapter.tables ? Dynamoid::Adapter.tables.include?(table_name) : false
54
+ end
55
+
56
+ def from_database(attrs = {})
57
+ clazz = attrs[:type] ? obj = attrs[:type].constantize : self
58
+ clazz.new(attrs).tap { |r| r.new_record = false }
59
+ end
60
+
61
+ # Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
62
+ #
63
+ # @since 0.2.0
64
+ def undump(incoming = nil)
65
+ incoming = (incoming || {}).symbolize_keys
66
+ Hash.new.tap do |hash|
67
+ self.attributes.each do |attribute, options|
68
+ hash[attribute] = undump_field(incoming[attribute], options)
69
+ end
70
+ incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute }
71
+ end
72
+ end
73
+
74
+ # Undump a value for a given type. Given a string, it'll determine (based on the type provided) whether to turn it into a
75
+ # string, integer, float, set, array, datetime, or serialized return value.
76
+ #
77
+ # @since 0.2.0
78
+ def undump_field(value, options)
79
+ if value.nil? && (default_value = options[:default])
80
+ value = default_value.respond_to?(:call) ? default_value.call : default_value
81
+ else
82
+ return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
83
+ end
84
+
85
+ case options[:type]
86
+ when :string
87
+ value.to_s
88
+ when :integer
89
+ value.to_i
90
+ when :float
91
+ value.to_f
92
+ when :set, :array
93
+ if value.is_a?(Set) || value.is_a?(Array)
94
+ value
95
+ else
96
+ Set[value]
97
+ end
98
+ when :datetime
99
+ if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
100
+ value
101
+ else
102
+ Time.at(value).to_datetime
103
+ end
104
+ when :serialized
105
+ if value.is_a?(String)
106
+ options[:serializer] ? options[:serializer].load(value) : YAML.load(value)
107
+ else
108
+ value
109
+ end
110
+ when :boolean
111
+ # persisted as 't', but because undump is called during initialize it can come in as true
112
+ if value == 't' || value == true
113
+ true
114
+ elsif value == 'f' || value == false
115
+ false
116
+ else
117
+ raise ArgumentError, "Boolean column neither true nor false"
118
+ end
119
+ else
120
+ raise ArgumentError, "Unknown type #{options[:type]}"
121
+ end
122
+ end
123
+
124
+ def dynamo_type(type)
125
+ case type
126
+ when :integer, :float, :datetime
127
+ :number
128
+ when :string, :serialized
129
+ :string
130
+ else
131
+ raise 'unknown type'
132
+ end
133
+ end
134
+
135
+ end
136
+
137
+ # Set updated_at and any passed in field to current DateTime. Useful for things like last_login_at, etc.
138
+ #
139
+ def touch(name = nil)
140
+ now = DateTime.now
141
+ self.updated_at = now
142
+ attributes[name] = now if name
143
+ save
144
+ end
145
+
146
+ # Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
147
+ #
148
+ # @since 0.2.0
149
+ def persisted?
150
+ !new_record?
151
+ end
152
+
153
+ # Run the callbacks and then persist this object in the datastore.
154
+ #
155
+ # @since 0.2.0
156
+ def save(options = {})
157
+ self.class.create_table
158
+
159
+ if new_record?
160
+ conditions = { :unless_exists => [self.class.hash_key]}
161
+ conditions[:unless_exists] << range_key if(range_key)
162
+
163
+ run_callbacks(:create) { persist(conditions) }
164
+ else
165
+ persist
166
+ end
167
+
168
+ self
169
+ end
170
+
171
+ #
172
+ # update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
173
+ # never cause an update! to fail, but an update! may cause a concurrent save to fail.
174
+ #
175
+ #
176
+ def update!(conditions = {}, &block)
177
+ run_callbacks(:update) do
178
+ options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
179
+ new_attrs = Dynamoid::Adapter.update_item(self.class.table_name, self.hash_key, options.merge(:conditions => conditions)) do |t|
180
+ if(self.class.attributes[:lock_version])
181
+ raise "Optimistic locking cannot be used with Partitioning" if(Dynamoid::Config.partitioning)
182
+ t.add(lock_version: 1)
183
+ end
184
+
185
+ yield t
186
+ end
187
+ load(new_attrs)
188
+ end
189
+ end
190
+
191
+ def update(conditions = {}, &block)
192
+ update!(conditions, &block)
193
+ true
194
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
195
+ false
196
+ end
197
+
198
+ # Delete this object, but only after running callbacks for it.
199
+ #
200
+ # @since 0.2.0
201
+ def destroy
202
+ run_callbacks(:destroy) do
203
+ self.delete
204
+ end
205
+ self
206
+ end
207
+
208
+ # Delete this object from the datastore and all indexes.
209
+ #
210
+ # @since 0.2.0
211
+ def delete
212
+ delete_indexes
213
+ options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
214
+ Dynamoid::Adapter.delete(self.class.table_name, self.hash_key, options)
215
+ end
216
+
217
+ # Dump this object's attributes into hash form, fit to be persisted into the datastore.
218
+ #
219
+ # @since 0.2.0
220
+ def dump
221
+ Hash.new.tap do |hash|
222
+ self.class.attributes.each do |attribute, options|
223
+ hash[attribute] = dump_field(self.read_attribute(attribute), options)
224
+ end
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ # Determine how to dump this field. Given a value, it'll determine how to turn it into a value that can be
231
+ # persisted into the datastore.
232
+ #
233
+ # @since 0.2.0
234
+ def dump_field(value, options)
235
+ return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
236
+
237
+ case options[:type]
238
+ when :string
239
+ value.to_s
240
+ when :integer
241
+ value.to_i
242
+ when :float
243
+ value.to_f
244
+ when :set, :array
245
+ if value.is_a?(Set) || value.is_a?(Array)
246
+ value
247
+ else
248
+ Set[value]
249
+ end
250
+ when :datetime
251
+ value.to_time.to_f
252
+ when :serialized
253
+ options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
254
+ when :boolean
255
+ value.to_s[0]
256
+ else
257
+ raise ArgumentError, "Unknown type #{options[:type]}"
258
+ end
259
+ end
260
+
261
+ # Persist the object into the datastore. Assign it an id first if it doesn't have one; then afterwards,
262
+ # save its indexes.
263
+ #
264
+ # @since 0.2.0
265
+ def persist(conditions = nil)
266
+ run_callbacks(:save) do
267
+ self.hash_key = SecureRandom.uuid if self.hash_key.nil? || self.hash_key.blank?
268
+
269
+ # Add an exists check to prevent overwriting existing records with new ones
270
+ if(new_record?)
271
+ conditions ||= {}
272
+ (conditions[:unless_exists] ||= []) << self.class.hash_key
273
+ end
274
+
275
+ # Add an optimistic locking check if the lock_version column exists
276
+ if(self.class.attributes[:lock_version])
277
+ conditions ||= {}
278
+ raise "Optimistic locking cannot be used with Partitioning" if(Dynamoid::Config.partitioning)
279
+ self.lock_version = (lock_version || 0) + 1
280
+ #Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
281
+ (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if(changes[:lock_version][0])
282
+ end
283
+
284
+ Dynamoid::Adapter.write(self.class.table_name, self.dump, conditions)
285
+ save_indexes
286
+ @new_record = false
287
+ true
288
+ end
289
+ end
290
+ end
291
+
292
+ end