adept_dynamoid 0.5.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Dynamoid.gemspec +193 -0
  4. data/Gemfile +23 -0
  5. data/Gemfile.lock +86 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.markdown +265 -0
  8. data/Rakefile +62 -0
  9. data/VERSION +1 -0
  10. data/doc/.nojekyll +0 -0
  11. data/doc/Dynamoid.html +312 -0
  12. data/doc/Dynamoid/Adapter.html +1385 -0
  13. data/doc/Dynamoid/Adapter/AwsSdk.html +1585 -0
  14. data/doc/Dynamoid/Adapter/Local.html +1574 -0
  15. data/doc/Dynamoid/Associations.html +131 -0
  16. data/doc/Dynamoid/Associations/Association.html +794 -0
  17. data/doc/Dynamoid/Associations/BelongsTo.html +158 -0
  18. data/doc/Dynamoid/Associations/ClassMethods.html +723 -0
  19. data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +164 -0
  20. data/doc/Dynamoid/Associations/HasMany.html +164 -0
  21. data/doc/Dynamoid/Associations/HasOne.html +158 -0
  22. data/doc/Dynamoid/Associations/ManyAssociation.html +1640 -0
  23. data/doc/Dynamoid/Associations/SingleAssociation.html +598 -0
  24. data/doc/Dynamoid/Components.html +204 -0
  25. data/doc/Dynamoid/Config.html +395 -0
  26. data/doc/Dynamoid/Config/Options.html +609 -0
  27. data/doc/Dynamoid/Criteria.html +131 -0
  28. data/doc/Dynamoid/Criteria/Chain.html +1063 -0
  29. data/doc/Dynamoid/Criteria/ClassMethods.html +98 -0
  30. data/doc/Dynamoid/Document.html +666 -0
  31. data/doc/Dynamoid/Document/ClassMethods.html +937 -0
  32. data/doc/Dynamoid/Errors.html +118 -0
  33. data/doc/Dynamoid/Errors/DocumentNotValid.html +210 -0
  34. data/doc/Dynamoid/Errors/Error.html +130 -0
  35. data/doc/Dynamoid/Errors/InvalidField.html +133 -0
  36. data/doc/Dynamoid/Errors/MissingRangeKey.html +133 -0
  37. data/doc/Dynamoid/Fields.html +669 -0
  38. data/doc/Dynamoid/Fields/ClassMethods.html +309 -0
  39. data/doc/Dynamoid/Finders.html +128 -0
  40. data/doc/Dynamoid/Finders/ClassMethods.html +516 -0
  41. data/doc/Dynamoid/Indexes.html +308 -0
  42. data/doc/Dynamoid/Indexes/ClassMethods.html +353 -0
  43. data/doc/Dynamoid/Indexes/Index.html +1104 -0
  44. data/doc/Dynamoid/Persistence.html +651 -0
  45. data/doc/Dynamoid/Persistence/ClassMethods.html +670 -0
  46. data/doc/Dynamoid/Validations.html +399 -0
  47. data/doc/_index.html +461 -0
  48. data/doc/class_list.html +47 -0
  49. data/doc/css/common.css +1 -0
  50. data/doc/css/full_list.css +55 -0
  51. data/doc/css/style.css +322 -0
  52. data/doc/file.LICENSE.html +66 -0
  53. data/doc/file.README.html +312 -0
  54. data/doc/file_list.html +52 -0
  55. data/doc/frames.html +13 -0
  56. data/doc/index.html +312 -0
  57. data/doc/js/app.js +205 -0
  58. data/doc/js/full_list.js +173 -0
  59. data/doc/js/jquery.js +16 -0
  60. data/doc/method_list.html +1238 -0
  61. data/doc/top-level-namespace.html +105 -0
  62. data/lib/dynamoid.rb +47 -0
  63. data/lib/dynamoid/adapter.rb +177 -0
  64. data/lib/dynamoid/adapter/aws_sdk.rb +223 -0
  65. data/lib/dynamoid/associations.rb +106 -0
  66. data/lib/dynamoid/associations/association.rb +105 -0
  67. data/lib/dynamoid/associations/belongs_to.rb +44 -0
  68. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
  69. data/lib/dynamoid/associations/has_many.rb +39 -0
  70. data/lib/dynamoid/associations/has_one.rb +39 -0
  71. data/lib/dynamoid/associations/many_association.rb +191 -0
  72. data/lib/dynamoid/associations/single_association.rb +69 -0
  73. data/lib/dynamoid/components.rb +36 -0
  74. data/lib/dynamoid/config.rb +57 -0
  75. data/lib/dynamoid/config/options.rb +78 -0
  76. data/lib/dynamoid/criteria.rb +29 -0
  77. data/lib/dynamoid/criteria/chain.rb +243 -0
  78. data/lib/dynamoid/dirty.rb +41 -0
  79. data/lib/dynamoid/document.rb +184 -0
  80. data/lib/dynamoid/errors.rb +28 -0
  81. data/lib/dynamoid/fields.rb +130 -0
  82. data/lib/dynamoid/finders.rb +131 -0
  83. data/lib/dynamoid/identity_map.rb +96 -0
  84. data/lib/dynamoid/indexes.rb +69 -0
  85. data/lib/dynamoid/indexes/index.rb +103 -0
  86. data/lib/dynamoid/middleware/identity_map.rb +16 -0
  87. data/lib/dynamoid/persistence.rb +247 -0
  88. data/lib/dynamoid/validations.rb +36 -0
  89. data/spec/app/models/address.rb +10 -0
  90. data/spec/app/models/camel_case.rb +24 -0
  91. data/spec/app/models/magazine.rb +11 -0
  92. data/spec/app/models/message.rb +9 -0
  93. data/spec/app/models/sponsor.rb +8 -0
  94. data/spec/app/models/subscription.rb +12 -0
  95. data/spec/app/models/tweet.rb +12 -0
  96. data/spec/app/models/user.rb +26 -0
  97. data/spec/dynamoid/adapter/aws_sdk_spec.rb +186 -0
  98. data/spec/dynamoid/adapter_spec.rb +117 -0
  99. data/spec/dynamoid/associations/association_spec.rb +194 -0
  100. data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
  101. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
  102. data/spec/dynamoid/associations/has_many_spec.rb +42 -0
  103. data/spec/dynamoid/associations/has_one_spec.rb +45 -0
  104. data/spec/dynamoid/associations_spec.rb +16 -0
  105. data/spec/dynamoid/config_spec.rb +27 -0
  106. data/spec/dynamoid/criteria/chain_spec.rb +140 -0
  107. data/spec/dynamoid/criteria_spec.rb +72 -0
  108. data/spec/dynamoid/dirty_spec.rb +49 -0
  109. data/spec/dynamoid/document_spec.rb +118 -0
  110. data/spec/dynamoid/fields_spec.rb +127 -0
  111. data/spec/dynamoid/finders_spec.rb +135 -0
  112. data/spec/dynamoid/identity_map_spec.rb +45 -0
  113. data/spec/dynamoid/indexes/index_spec.rb +104 -0
  114. data/spec/dynamoid/indexes_spec.rb +25 -0
  115. data/spec/dynamoid/persistence_spec.rb +176 -0
  116. data/spec/dynamoid/validations_spec.rb +36 -0
  117. data/spec/dynamoid_spec.rb +9 -0
  118. data/spec/spec_helper.rb +50 -0
  119. metadata +376 -0
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+
4
+ module Associations
5
+ module SingleAssociation
6
+ include Association
7
+
8
+ delegate :class, :to => :target
9
+
10
+ def setter(object)
11
+ delete
12
+ source.update_attribute(source_attribute, Set[object.id])
13
+ self.send(:associate_target, object) if target_association
14
+ object
15
+ end
16
+
17
+ def delete
18
+ source.update_attribute(source_attribute, nil)
19
+ self.send(:disassociate_target, target) if target && target_association
20
+ target
21
+ end
22
+
23
+ def create!(attributes = {})
24
+ setter(target_class.create!(attributes))
25
+ end
26
+
27
+ def create(attributes = {})
28
+ setter(target_class.create!(attributes))
29
+ end
30
+
31
+
32
+ # Is this object equal to the association's target?
33
+ #
34
+ # @return [Boolean] true/false
35
+ #
36
+ # @since 0.2.0
37
+ def ==(other)
38
+ target == other
39
+ end
40
+
41
+ # Delegate methods we don't find directly to the target.
42
+ #
43
+ # @since 0.2.0
44
+ def method_missing(method, *args)
45
+ if target.respond_to?(method)
46
+ target.send(method, *args)
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def nil?
53
+ target.nil?
54
+ end
55
+
56
+ private
57
+
58
+ # Find the target of the has_one association.
59
+ #
60
+ # @return [Dynamoid::Document] the found target (or nil if nothing)
61
+ #
62
+ # @since 0.2.0
63
+ def find_target
64
+ return if source_ids.empty?
65
+ target_class.find(source_ids.first)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+ module Dynamoid
3
+
4
+ # All modules that a Document is composed of are defined in this
5
+ # module, to keep the document class from getting too cluttered.
6
+ module Components
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ extend ActiveModel::Translation
11
+ extend ActiveModel::Callbacks
12
+
13
+ define_model_callbacks :create, :save, :destroy, :initialize
14
+
15
+ before_create :set_created_at
16
+ before_save :set_updated_at
17
+ end
18
+
19
+ include ActiveModel::AttributeMethods
20
+ include ActiveModel::Conversion
21
+ include ActiveModel::MassAssignmentSecurity
22
+ include ActiveModel::Naming
23
+ include ActiveModel::Observing
24
+ include ActiveModel::Serializers::JSON
25
+ include ActiveModel::Serializers::Xml
26
+ include Dynamoid::Fields
27
+ include Dynamoid::Indexes
28
+ include Dynamoid::Persistence
29
+ include Dynamoid::Finders
30
+ include Dynamoid::Associations
31
+ include Dynamoid::Criteria
32
+ include Dynamoid::Validations
33
+ include Dynamoid::IdentityMap
34
+ include Dynamoid::Dirty
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+ require "uri"
3
+ require "dynamoid/config/options"
4
+
5
+ module Dynamoid
6
+
7
+ # Contains all the basic configuration information required for Dynamoid: both sensible defaults and required fields.
8
+ module Config
9
+ extend self
10
+ extend Options
11
+ include ActiveModel::Observing
12
+
13
+ # All the default options.
14
+ option :adapter, :default => 'aws-sdk'
15
+ option :namespace, :default => defined?(Rails) ? "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" : "dynamoid"
16
+ option :logger, :default => defined?(Rails)
17
+ option :access_key
18
+ option :secret_key
19
+ option :read_capacity, :default => 100
20
+ option :write_capacity, :default => 20
21
+ option :warn_on_scan, :default => true
22
+ option :partitioning, :default => false
23
+ option :partition_size, :default => 200
24
+ option :endpoint, :default => 'dynamodb.us-east-1.amazonaws.com'
25
+ option :use_ssl, :default => true
26
+ option :port, :default => '443'
27
+ option :included_models, :default => []
28
+ option :identity_map, :default => false
29
+
30
+ # The default logger for Dynamoid: either the Rails logger or just stdout.
31
+ #
32
+ # @since 0.2.0
33
+ def default_logger
34
+ defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
35
+ end
36
+
37
+ # Returns the assigned logger instance.
38
+ #
39
+ # @since 0.2.0
40
+ def logger
41
+ @logger ||= default_logger
42
+ end
43
+
44
+ # If you want to, set the logger manually to any output you'd like. Or pass false or nil to disable logging entirely.
45
+ #
46
+ # @since 0.2.0
47
+ def logger=(logger)
48
+ case logger
49
+ when false, nil then @logger = nil
50
+ when true then @logger = default_logger
51
+ else
52
+ @logger = logger if logger.respond_to?(:info)
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,78 @@
1
+ # Shamelessly stolen from Mongoid!
2
+ module Dynamoid #:nodoc
3
+ module Config
4
+
5
+ # Encapsulates logic for setting options.
6
+ module Options
7
+
8
+ # Get the defaults or initialize a new empty hash.
9
+ #
10
+ # @example Get the defaults.
11
+ # options.defaults
12
+ #
13
+ # @return [ Hash ] The default options.
14
+ #
15
+ # @since 0.2.0
16
+ def defaults
17
+ @defaults ||= {}
18
+ end
19
+
20
+ # Define a configuration option with a default.
21
+ #
22
+ # @example Define the option.
23
+ # Options.option(:persist_in_safe_mode, :default => false)
24
+ #
25
+ # @param [ Symbol ] name The name of the configuration option.
26
+ # @param [ Hash ] options Extras for the option.
27
+ #
28
+ # @option options [ Object ] :default The default value.
29
+ #
30
+ # @since 0.2.0
31
+ def option(name, options = {})
32
+ defaults[name] = settings[name] = options[:default]
33
+
34
+ class_eval <<-RUBY
35
+ def #{name}
36
+ settings[#{name.inspect}]
37
+ end
38
+
39
+ def #{name}=(value)
40
+ settings[#{name.inspect}] = value
41
+ end
42
+
43
+ def #{name}?
44
+ #{name}
45
+ end
46
+
47
+ def reset_#{name}
48
+ settings[#{name.inspect}] = defaults[#{name.inspect}]
49
+ end
50
+ RUBY
51
+ end
52
+
53
+ # Reset the configuration options to the defaults.
54
+ #
55
+ # @example Reset the configuration options.
56
+ # config.reset
57
+ #
58
+ # @return [ Hash ] The defaults.
59
+ #
60
+ # @since 0.2.0
61
+ def reset
62
+ settings.replace(defaults)
63
+ end
64
+
65
+ # Get the settings or initialize a new empty hash.
66
+ #
67
+ # @example Get the settings.
68
+ # options.settings
69
+ #
70
+ # @return [ Hash ] The setting options.
71
+ #
72
+ # @since 0.2.0
73
+ def settings
74
+ @settings ||= {}
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ require 'dynamoid/criteria/chain'
3
+
4
+ module Dynamoid
5
+
6
+ # Allows classes to be queried by where, all, first, and each and return criteria chains.
7
+ module Criteria
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+
12
+ [:where, :all, :first, :each, :limit, :start, :scan_index_forward].each do |meth|
13
+ # Return a criteria chain in response to a method that will begin or end a chain. For more information,
14
+ # see Dynamoid::Criteria::Chain.
15
+ #
16
+ # @since 0.2.0
17
+ define_method(meth) do |*args|
18
+ chain = Dynamoid::Criteria::Chain.new(self)
19
+ if args
20
+ chain.send(meth, *args)
21
+ else
22
+ chain.send(meth)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,243 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+ module Criteria
4
+
5
+ # The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from
6
+ # chain to relation). It is a chainable object that builds up a query and eventually executes it either on an index
7
+ # or by a full table scan.
8
+ class Chain
9
+ attr_accessor :query, :source, :index, :values, :limit, :start, :consistent_read
10
+ include Enumerable
11
+
12
+ # Create a new criteria chain.
13
+ #
14
+ # @param [Class] source the class upon which the ultimate query will be performed.
15
+ def initialize(source)
16
+ @query = {}
17
+ @source = source
18
+ @consistent_read = false
19
+ @scan_index_forward = true
20
+ end
21
+
22
+ # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
23
+ # ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
24
+ # an attribute name with a range operator.
25
+ #
26
+ # @example A simple criteria
27
+ # where(:name => 'Josh')
28
+ #
29
+ # @example A more complicated criteria
30
+ # where(:name => 'Josh', 'created_at.gt' => DateTime.now - 1.day)
31
+ #
32
+ # @since 0.2.0
33
+ def where(args)
34
+ args.each {|k, v| query[k] = v}
35
+ self
36
+ end
37
+
38
+ def consistent
39
+ @consistent_read = true
40
+ self
41
+ end
42
+
43
+ # Returns all the records matching the criteria.
44
+ #
45
+ # @since 0.2.0
46
+ def all
47
+ records
48
+ end
49
+
50
+ # Returns the first record matching the criteria.
51
+ #
52
+ # @since 0.2.0
53
+ def first
54
+ limit(1).first
55
+ end
56
+
57
+ def limit(limit)
58
+ @limit = limit
59
+ records
60
+ end
61
+
62
+ def start(start)
63
+ @start = start
64
+ self
65
+ end
66
+
67
+ def scan_index_forward(scan_index_forward)
68
+ @scan_index_forward = scan_index_forward
69
+ self
70
+ end
71
+
72
+ # Allows you to use the results of a search as an enumerable over the results found.
73
+ #
74
+ # @since 0.2.0
75
+ def each(&block)
76
+ records.each(&block)
77
+ end
78
+
79
+ def consistent_opts
80
+ { :consistent_read => consistent_read }
81
+ end
82
+
83
+ private
84
+
85
+ # The actual records referenced by the association.
86
+ #
87
+ # @return [Array] an array of the found records.
88
+ #
89
+ # @since 0.2.0
90
+ def records
91
+ if range?
92
+ records_with_range
93
+ elsif index
94
+ records_with_index
95
+ else
96
+ records_without_index
97
+ end
98
+ end
99
+
100
+ # If the query matches an index on the associated class, then this method will retrieve results from the index table.
101
+ #
102
+ # @return [Array] an array of the found records.
103
+ #
104
+ # @since 0.2.0
105
+ def records_with_index
106
+ ids = ids_from_index
107
+ if ids.nil? || ids.empty?
108
+ []
109
+ else
110
+ ids = ids.to_a
111
+
112
+ if @start
113
+ ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
114
+ end
115
+
116
+ ids = ids.take(@limit) if @limit
117
+ Array(source.find(ids, consistent_opts))
118
+ end
119
+ end
120
+
121
+ # Returns the Set of IDs from the index table.
122
+ #
123
+ # @return [Set] a Set containing the IDs from the index.
124
+ def ids_from_index
125
+ if index.range_key?
126
+ Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts)).inject(Set.new) do |all, record|
127
+ all + Set.new(record[:ids])
128
+ end
129
+ else
130
+ results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
131
+ results ? results[:ids] : []
132
+ end
133
+ end
134
+
135
+ def records_with_range
136
+ Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.from_database(hash) }
137
+ end
138
+
139
+ # If the query does not match an index, we'll manually scan the associated table to find results.
140
+ #
141
+ # @return [Array] an array of the found records.
142
+ #
143
+ # @since 0.2.0
144
+ def records_without_index
145
+ if Dynamoid::Config.warn_on_scan
146
+ Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
147
+ 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(', ')}]"
148
+ end
149
+
150
+ if @consistent_read
151
+ raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
152
+ end
153
+
154
+ Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.from_database(hash) }
155
+ end
156
+
157
+ # Format the provided query so that it can be used to query results from DynamoDB.
158
+ #
159
+ # @return [Hash] a hash with keys of :hash_value and :range_value
160
+ #
161
+ # @since 0.2.0
162
+ def index_query
163
+ values = index.values(query)
164
+ {}.tap do |hash|
165
+ hash[:hash_value] = values[:hash_value]
166
+ if index.range_key?
167
+ key = query.keys.find{|k| k.to_s.include?('.')}
168
+ if key
169
+ hash.merge!(range_hash(key))
170
+ else
171
+ raise Dynamoid::Errors::MissingRangeKey, 'This index requires a range key'
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def range_hash(key)
178
+ val = query[key]
179
+
180
+ return { :range_value => query[key] } if query[key].is_a?(Range)
181
+
182
+ case key.split('.').last
183
+ when 'gt'
184
+ { :range_greater_than => val.to_f }
185
+ when 'lt'
186
+ { :range_less_than => val.to_f }
187
+ when 'gte'
188
+ { :range_gte => val.to_f }
189
+ when 'lte'
190
+ { :range_lte => val.to_f }
191
+ when 'begins_with'
192
+ { :range_begins_with => val }
193
+ end
194
+ end
195
+
196
+ def range_query
197
+ opts = { :hash_value => query[source.hash_key] }
198
+ if key = query.keys.find { |k| k.to_s.include?('.') }
199
+ opts.merge!(range_hash(key))
200
+ end
201
+ opts.merge(query_opts).merge(consistent_opts)
202
+ end
203
+
204
+ # Return an index that fulfills all the attributes the criteria is querying, or nil if none is found.
205
+ #
206
+ # @since 0.2.0
207
+ def index
208
+ index = source.find_index(query_keys)
209
+ return nil if index.blank?
210
+ index
211
+ end
212
+
213
+ def query_keys
214
+ query.keys.collect{|k| k.to_s.split('.').first}
215
+ end
216
+
217
+ def range?
218
+ return false unless source.range_key
219
+ query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
220
+ end
221
+
222
+ def start_key
223
+ hash_key_type = @start.class.attributes[@start.class.hash_key][:type] == :string ? 'S' : 'N'
224
+ key = { :hash_key_element => { hash_key_type => @start.hash_key.to_s } }
225
+ if range_key = @start.class.range_key
226
+ range_key_type = @start.class.attributes[range_key][:type] == :string ? 'S' : 'N'
227
+ key.merge!({:range_key_element => { range_key_type => @start.send(range_key).to_s } })
228
+ end
229
+ key
230
+ end
231
+
232
+ def query_opts
233
+ opts = {}
234
+ opts[:limit] = @limit if @limit
235
+ opts[:next_token] = start_key if @start
236
+ opts[:scan_index_forward] = @scan_index_forward
237
+ opts
238
+ end
239
+ end
240
+
241
+ end
242
+
243
+ end