lib/dynamoid/criteria/chain.rb in dynamoid-0.3.2 vs lib/dynamoid/criteria/chain.rb in dynamoid-0.4.0
- old
+ new
@@ -4,24 +4,25 @@
# The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from
# chain to relation). It is a chainable object that builds up a query and eventually executes it either on an index
# or by a full table scan.
class Chain
- attr_accessor :query, :source, :index, :values
+ attr_accessor :query, :source, :index, :values, :limit, :start, :consistent_read
include Enumerable
-
+
# Create a new criteria chain.
#
# @param [Class] source the class upon which the ultimate query will be performed.
def initialize(source)
@query = {}
@source = source
+ @consistent_read = false
end
-
- # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
- # ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
- # an attribute name with a range operator.
+
+ # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
+ # ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
+ # an attribute name with a range operator.
#
# @example A simple criteria
# where(:name => 'Josh')
#
# @example A more complicated criteria
@@ -30,122 +31,196 @@
# @since 0.2.0
def where(args)
args.each {|k, v| query[k] = v}
self
end
-
+
+ def consistent
+ @consistent_read = true
+ self
+ end
+
# Returns all the records matching the criteria.
#
# @since 0.2.0
def all
records
end
# Returns the first record matching the criteria.
#
- # @since 0.2.0
+ # @since 0.2.0
def first
- records.first
+ limit(1).first
end
+ def limit(limit)
+ @limit = limit
+ records
+ end
+
+ def start(start)
+ @start = start
+ self
+ end
+
# Allows you to use the results of a search as an enumerable over the results found.
#
- # @since 0.2.0
+ # @since 0.2.0
def each(&block)
records.each(&block)
end
-
+
+ def consistent_opts
+ { :consistent_read => consistent_read }
+ end
+
private
-
+
# The actual records referenced by the association.
#
# @return [Array] an array of the found records.
#
# @since 0.2.0
def records
- return records_with_index if index
- records_without_index
+ if range?
+ records_with_range
+ elsif index
+ records_with_index
+ else
+ records_without_index
+ end
end
# If the query matches an index on the associated class, then this method will retrieve results from the index table.
#
# @return [Array] an array of the found records.
#
- # @since 0.2.0
+ # @since 0.2.0
def records_with_index
ids = if index.range_key?
Dynamoid::Adapter.query(index.table_name, index_query).collect{|r| r[:ids]}.inject(Set.new) {|set, result| set + result}
else
- results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value])
+ results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
if results
results[:ids]
else
[]
end
end
if ids.nil? || ids.empty?
[]
else
- Array(source.find(ids.to_a))
+ ids = ids.to_a
+
+ if @start
+ ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
+ end
+
+ ids = ids.take(@limit) if @limit
+ Array(source.find(ids, consistent_opts))
end
end
-
- # If the query does not match an index, we'll manually scan the associated table to manually find results.
+
+ def records_with_range
+ Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
+ end
+
+ # If the query does not match an index, we'll manually scan the associated table to find results.
#
# @return [Array] an array of the found records.
#
- # @since 0.2.0
+ # @since 0.2.0
def records_without_index
if Dynamoid::Config.warn_on_scan
Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
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(', ')}]"
end
- Dynamoid::Adapter.scan(source.table_name, query).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
+
+ Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.new(hash).tap { |r| r.new_record = false } }
end
-
- # Format the provided query so that it can be used to query results from DynamoDB.
+
+ # Format the provided query so that it can be used to query results from DynamoDB.
#
# @return [Hash] a hash with keys of :hash_value and :range_value
#
- # @since 0.2.0
+ # @since 0.2.0
def index_query
values = index.values(query)
{}.tap do |hash|
hash[:hash_value] = values[:hash_value]
if index.range_key?
key = query.keys.find{|k| k.to_s.include?('.')}
if key
- if query[key].is_a?(Range)
- hash[:range_value] = query[key]
- else
- val = query[key].to_f
- case key.split('.').last
- when 'gt'
- hash[:range_greater_than] = val
- when 'lt'
- hash[:range_less_than] = val
- when 'gte'
- hash[:range_gte] = val
- when 'lte'
- hash[:range_lte] = val
- end
- end
+ hash.merge!(range_hash(key))
else
raise Dynamoid::Errors::MissingRangeKey, 'This index requires a range key'
end
end
end
end
+ def range_hash(key)
+ val = query[key]
+
+ return { :range_value => query[key] } if query[key].is_a?(Range)
+
+ case key.split('.').last
+ when 'gt'
+ { :range_greater_than => val.to_f }
+ when 'lt'
+ { :range_less_than => val.to_f }
+ when 'gte'
+ { :range_gte => val.to_f }
+ when 'lte'
+ { :range_lte => val.to_f }
+ when 'begins_with'
+ { :range_begins_with => val }
+ end
+ end
+
+ def range_query
+ opts = { :hash_value => query[source.hash_key] }
+ if key = query.keys.find { |k| k.to_s.include?('.') }
+ opts.merge!(range_key(key))
+ end
+ opts.merge(query_opts).merge(consistent_opts)
+ end
+
# Return an index that fulfills all the attributes the criteria is querying, or nil if none is found.
#
- # @since 0.2.0
+ # @since 0.2.0
def index
- index = source.find_index(query.keys.collect{|k| k.to_s.split('.').first})
+ index = source.find_index(query_keys)
return nil if index.blank?
index
end
+
+ def query_keys
+ query.keys.collect{|k| k.to_s.split('.').first}
+ end
+
+ def range?
+ return false unless source.range_key
+ query_keys == ['id'] || (query_keys.to_set == ['id', source.range_key.to_s].to_set)
+ end
+
+ def start_key
+ key = { :hash_key_element => { 'S' => @start.hash_key } }
+ if range_key = @start.class.range_key
+ range_key_type = @start.class.attributes[range_key][:type] == :string ? 'S' : 'N'
+ key.merge!({:range_key_element => { range_key_type => @start.send(range_key) } })
+ end
+ key
+ end
+
+ def query_opts
+ opts = {}
+ opts[:limit] = @limit if @limit
+ opts[:next_token] = start_key if @start
+ opts
+ end
end
-
+
end
-
+
end