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,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,37 @@
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, :update
14
+
15
+ before_create :set_created_at
16
+ before_save :set_updated_at
17
+ after_initialize :set_type
18
+ end
19
+
20
+ include ActiveModel::AttributeMethods
21
+ include ActiveModel::Conversion
22
+ include ActiveModel::MassAssignmentSecurity if defined?(ActiveModel::MassAssignmentSecurity)
23
+ include ActiveModel::Naming
24
+ include ActiveModel::Observing if defined?(ActiveModel::Observing)
25
+ include ActiveModel::Serializers::JSON
26
+ include ActiveModel::Serializers::Xml
27
+ include Dynamoid::Fields
28
+ include Dynamoid::Indexes
29
+ include Dynamoid::Persistence
30
+ include Dynamoid::Finders
31
+ include Dynamoid::Associations
32
+ include Dynamoid::Criteria
33
+ include Dynamoid::Validations
34
+ include Dynamoid::IdentityMap
35
+ include Dynamoid::Dirty
36
+ end
37
+ 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 if defined?(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,326 @@
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.to_sym] = 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(opts = {})
47
+ batch opts[:batch_size] if opts.has_key? :batch_size
48
+ records
49
+ end
50
+
51
+ # Destroys all the records matching the criteria.
52
+ #
53
+ def destroy_all
54
+ ids = []
55
+
56
+ if range?
57
+ ranges = []
58
+ Dynamoid::Adapter.query(source.table_name, range_query).collect do |hash|
59
+ ids << hash[source.hash_key.to_sym]
60
+ ranges << hash[source.range_key.to_sym]
61
+ end
62
+
63
+ Dynamoid::Adapter.delete(source.table_name, ids,{:range_key => ranges})
64
+ elsif index
65
+ #TODO: test this throughly and find a way to delete all index table records for one source record
66
+ if index.range_key?
67
+ results = Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts))
68
+ else
69
+ results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
70
+ end
71
+
72
+ results.collect do |hash|
73
+ ids << hash[source.hash_key.to_sym]
74
+ index_ranges << hash[source.range_key.to_sym]
75
+ end
76
+
77
+ unless ids.nil? || ids.empty?
78
+ ids = ids.to_a
79
+
80
+ if @start
81
+ ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
82
+ index_ranges = index_ranges.drop_while { |range| range != @start.hash_key }.drop(1) unless index_ranges.nil?
83
+ end
84
+
85
+ if @limit
86
+ ids = ids.take(@limit)
87
+ index_ranges = index_ranges.take(@limit)
88
+ end
89
+
90
+ Dynamoid::Adapter.delete(source.table_name, ids)
91
+
92
+ if index.range_key?
93
+ Dynamoid::Adapter.delete(index.table_name, ids,{:range_key => index_ranges})
94
+ else
95
+ Dynamoid::Adapter.delete(index.table_name, ids)
96
+ end
97
+
98
+ end
99
+ else
100
+ Dynamoid::Adapter.scan(source.table_name, query, scan_opts).collect do |hash|
101
+ ids << hash[source.hash_key.to_sym]
102
+ end
103
+
104
+ Dynamoid::Adapter.delete(source.table_name, ids)
105
+ end
106
+ end
107
+
108
+ # Returns the first record matching the criteria.
109
+ #
110
+ # @since 0.2.0
111
+ def first
112
+ limit(1).first
113
+ end
114
+
115
+ def limit(limit)
116
+ @limit = limit
117
+ records
118
+ end
119
+
120
+ def batch(batch_size)
121
+ raise 'Cannot batch calls when using partitioning' if Dynamoid::Config.partitioning?
122
+ @batch_size = batch_size
123
+ self
124
+ end
125
+
126
+ def start(start)
127
+ @start = start
128
+ self
129
+ end
130
+
131
+ def scan_index_forward(scan_index_forward)
132
+ @scan_index_forward = scan_index_forward
133
+ self
134
+ end
135
+
136
+ # Allows you to use the results of a search as an enumerable over the results found.
137
+ #
138
+ # @since 0.2.0
139
+ def each(&block)
140
+ records.each(&block)
141
+ end
142
+
143
+ def consistent_opts
144
+ { :consistent_read => consistent_read }
145
+ end
146
+
147
+ private
148
+
149
+ # The actual records referenced by the association.
150
+ #
151
+ # @return [Enumerator] an iterator of the found records.
152
+ #
153
+ # @since 0.2.0
154
+ def records
155
+ results = if range?
156
+ records_with_range
157
+ elsif index
158
+ records_with_index
159
+ else
160
+ records_without_index
161
+ end
162
+ @batch_size ? results : Array(results)
163
+ end
164
+
165
+ # If the query matches an index on the associated class, then this method will retrieve results from the index table.
166
+ #
167
+ # @return [Enumerator] an iterator of the found records.
168
+ #
169
+ # @since 0.2.0
170
+ def records_with_index
171
+ ids = ids_from_index
172
+ if ids.nil? || ids.empty?
173
+ [].to_enum
174
+ else
175
+ ids = ids.to_a
176
+
177
+ if @start
178
+ ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
179
+ end
180
+
181
+ ids = ids.take(@limit) if @limit
182
+ source.find(ids, consistent_opts)
183
+ end
184
+ end
185
+
186
+ # Returns the Set of IDs from the index table.
187
+ #
188
+ # @return [Set] a Set containing the IDs from the index.
189
+ def ids_from_index
190
+ if index.range_key?
191
+ Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts)).inject(Set.new) do |all, record|
192
+ all + Set.new(record[:ids])
193
+ end
194
+ else
195
+ results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
196
+ results ? results[:ids] : []
197
+ end
198
+ end
199
+
200
+ def records_with_range
201
+ Enumerator.new do |yielder|
202
+ Dynamoid::Adapter.query(source.table_name, range_query).each do |hash|
203
+ yielder.yield source.from_database(hash)
204
+ end
205
+ end
206
+ end
207
+
208
+ # If the query does not match an index, we'll manually scan the associated table to find results.
209
+ #
210
+ # @return [Enumerator] an iterator of the found records.
211
+ #
212
+ # @since 0.2.0
213
+ def records_without_index
214
+ if Dynamoid::Config.warn_on_scan
215
+ Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
216
+ 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(', ')}]"
217
+ end
218
+
219
+ if @consistent_read
220
+ raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
221
+ end
222
+
223
+ Enumerator.new do |yielder|
224
+ Dynamoid::Adapter.scan(source.table_name, query, scan_opts).each do |hash|
225
+ yielder.yield source.from_database(hash)
226
+ end
227
+ end
228
+ end
229
+
230
+ # Format the provided query so that it can be used to query results from DynamoDB.
231
+ #
232
+ # @return [Hash] a hash with keys of :hash_value and :range_value
233
+ #
234
+ # @since 0.2.0
235
+ def index_query
236
+ values = index.values(query)
237
+ {}.tap do |hash|
238
+ hash[:hash_value] = values[:hash_value]
239
+ if index.range_key?
240
+ key = query.keys.find{|k| k.to_s.include?('.')}
241
+ if key
242
+ hash.merge!(range_hash(key))
243
+ else
244
+ raise Dynamoid::Errors::MissingRangeKey, 'This index requires a range key'
245
+ end
246
+ end
247
+ end
248
+ end
249
+
250
+ def range_hash(key)
251
+ val = query[key]
252
+
253
+ return { :range_value => query[key] } if query[key].is_a?(Range)
254
+
255
+ case key.split('.').last
256
+ when 'gt'
257
+ { :range_greater_than => val.to_f }
258
+ when 'lt'
259
+ { :range_less_than => val.to_f }
260
+ when 'gte'
261
+ { :range_gte => val.to_f }
262
+ when 'lte'
263
+ { :range_lte => val.to_f }
264
+ when 'begins_with'
265
+ { :range_begins_with => val }
266
+ end
267
+ end
268
+
269
+ def range_query
270
+ opts = { :hash_value => query[source.hash_key] }
271
+ if key = query.keys.find { |k| k.to_s.include?('.') }
272
+ opts.merge!(range_hash(key))
273
+ end
274
+ opts.merge(query_opts).merge(consistent_opts)
275
+ end
276
+
277
+ # Return an index that fulfills all the attributes the criteria is querying, or nil if none is found.
278
+ #
279
+ # @since 0.2.0
280
+ def index
281
+ index = source.find_index(query_keys)
282
+ return nil if index.blank?
283
+ index
284
+ end
285
+
286
+ def query_keys
287
+ query.keys.collect{|k| k.to_s.split('.').first}
288
+ end
289
+
290
+ # Use range query only when [hash_key] or [hash_key, range_key] is specified in query keys.
291
+ def range?
292
+ return false unless query_keys.include?(source.hash_key.to_s) or query_keys.include?(source.range_key.to_s)
293
+ query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
294
+ end
295
+
296
+ def start_key
297
+ hash_key_type = @start.class.attributes[@start.class.hash_key][:type] == :string ? 'S' : 'N'
298
+ key = { :hash_key_element => { hash_key_type => @start.hash_key.to_s } }
299
+ if range_key = @start.class.range_key
300
+ range_key_type = @start.class.attributes[range_key][:type] == :string ? 'S' : 'N'
301
+ key.merge!({:range_key_element => { range_key_type => @start.send(range_key).to_s } })
302
+ end
303
+ key
304
+ end
305
+
306
+ def query_opts
307
+ opts = {}
308
+ opts[:select] = :all
309
+ opts[:limit] = @limit if @limit
310
+ opts[:next_token] = start_key if @start
311
+ opts[:scan_index_forward] = @scan_index_forward
312
+ opts
313
+ end
314
+
315
+ def scan_opts
316
+ opts = {}
317
+ opts[:limit] = @limit if @limit
318
+ opts[:next_token] = start_key if @start
319
+ opts[:batch_size] = @batch_size if @batch_size
320
+ opts
321
+ end
322
+ end
323
+
324
+ end
325
+
326
+ end