class Dynamoid::Criteria::Chain

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.

Attributes

consistent_read[RW]
index[RW]
limit[RW]
query[RW]
source[RW]
start[RW]
values[RW]

Public Class Methods

new(source) click to toggle source

Create a new criteria chain.

@param [Class] source the class upon which the ultimate query will be performed.

# File lib/dynamoid/criteria/chain.rb, line 15
def initialize(source)
  @query = {}
  @source = source
  @consistent_read = false
  @scan_index_forward = true
end

Public Instance Methods

all(opts = {}) click to toggle source

Returns all the records matching the criteria.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 46
def all(opts = {})
  batch opts[:batch_size] if opts.has_key? :batch_size
  records
end
batch(batch_size) click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 120
def batch(batch_size)
  raise 'Cannot batch calls when using partitioning' if Dynamoid::Config.partitioning?
  @batch_size = batch_size
  self
end
consistent() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 38
def consistent
  @consistent_read = true
  self
end
consistent_opts() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 143
def consistent_opts
  { :consistent_read => consistent_read }
end
destroy_all() click to toggle source

Destroys all the records matching the criteria.

# File lib/dynamoid/criteria/chain.rb, line 53
def destroy_all
  ids = []
  
  if range?
    ranges = []
    Dynamoid::Adapter.query(source.table_name, range_query).collect do |hash| 
      ids << hash[source.hash_key.to_sym]
      ranges << hash[source.range_key.to_sym]
    end
    
    Dynamoid::Adapter.delete(source.table_name, ids,{:range_key => ranges})
  elsif index
    #TODO: test this throughly and find a way to delete all index table records for one source record
    if index.range_key?
      results = Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts))
    else
      results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
    end
    
    results.collect do |hash| 
      ids << hash[source.hash_key.to_sym]
      index_ranges << hash[source.range_key.to_sym]
    end
  
    unless ids.nil? || ids.empty?
      ids = ids.to_a
  
      if @start
        ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
        index_ranges = index_ranges.drop_while { |range| range != @start.hash_key }.drop(1) unless index_ranges.nil?
      end
  
      if @limit           
        ids = ids.take(@limit) 
        index_ranges = index_ranges.take(@limit)
      end
      
      Dynamoid::Adapter.delete(source.table_name, ids)
      
      if index.range_key?
        Dynamoid::Adapter.delete(index.table_name, ids,{:range_key => index_ranges})
      else
        Dynamoid::Adapter.delete(index.table_name, ids)
      end
      
    end
  else
    Dynamoid::Adapter.scan(source.table_name, query, scan_opts).collect do |hash| 
      ids << hash[source.hash_key.to_sym]
    end
    
    Dynamoid::Adapter.delete(source.table_name, ids)
  end   
end
each(&block) click to toggle source

Allows you to use the results of a search as an enumerable over the results found.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 139
def each(&block)
  records.each(&block)
end
first() click to toggle source

Returns the first record matching the criteria.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 111
def first
  limit(1).first
end
scan_index_forward(scan_index_forward) click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 131
def scan_index_forward(scan_index_forward)
  @scan_index_forward = scan_index_forward
  self
end
where(args) click to toggle source

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

where(:name => 'Josh', 'created_at.gt' => DateTime.now - 1.day)

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 33
def where(args)
  args.each {|k, v| query[k.to_sym] = v}
  self
end

Private Instance Methods

ids_from_index() click to toggle source

Returns the Set of IDs from the index table.

@return [Set] a Set containing the IDs from the index.

# File lib/dynamoid/criteria/chain.rb, line 189
def ids_from_index
  if index.range_key?
    Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts)).inject(Set.new) do |all, record|
      all + Set.new(record[:ids])
    end
  else
    results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
    results ? results[:ids] : []
  end
end
index_query() click to toggle source

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

# File lib/dynamoid/criteria/chain.rb, line 235
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
        hash.merge!(range_hash(key))
      else
        raise Dynamoid::Errors::MissingRangeKey, 'This index requires a range key'
      end
    end
  end
end
query_keys() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 286
def query_keys
  query.keys.collect{|k| k.to_s.split('.').first}
end
query_opts() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 306
def query_opts
  opts = {}
  opts[:select] = :all
  opts[:limit] = @limit if @limit
  opts[:next_token] = start_key if @start
  opts[:scan_index_forward] = @scan_index_forward
  opts
end
range?() click to toggle source

Use range query only when [hash_key] or [hash_key, range_key] is specified in query keys.

# File lib/dynamoid/criteria/chain.rb, line 291
def range?
  return false unless query_keys.include?(source.hash_key.to_s) or query_keys.include?(source.range_key.to_s)
  query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
end
range_hash(key) click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 250
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
range_query() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 269
def range_query
  opts = { :hash_value => query[source.hash_key] }
  if key = query.keys.find { |k| k.to_s.include?('.') }
    opts.merge!(range_hash(key))
  end
  opts.merge(query_opts).merge(consistent_opts)
end
records() click to toggle source

The actual records referenced by the association.

@return [Enumerator] an iterator of the found records.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 154
def records
  results = if range?
    records_with_range
  elsif index
    records_with_index
  else
    records_without_index
  end
  @batch_size ? results : Array(results)
end
records_with_index() click to toggle source

If the query matches an index on the associated class, then this method will retrieve results from the index table.

@return [Enumerator] an iterator of the found records.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 170
def records_with_index
  ids = ids_from_index
  if ids.nil? || ids.empty?
    [].to_enum
  else
    ids = ids.to_a

    if @start
      ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
    end

    ids = ids.take(@limit) if @limit
    source.find(ids, consistent_opts)
  end
end
records_with_range() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 200
def records_with_range
  Enumerator.new do |yielder|
    Dynamoid::Adapter.query(source.table_name, range_query).each do |hash|
      yielder.yield source.from_database(hash)
    end
  end
end
records_without_index() click to toggle source

If the query does not match an index, we’ll manually scan the associated table to find results.

@return [Enumerator] an iterator of the found records.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 213
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

  if @consistent_read
    raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
  end

  Enumerator.new do |yielder|
    Dynamoid::Adapter.scan(source.table_name, query, scan_opts).each do |hash|
      yielder.yield source.from_database(hash)
    end
  end
end
scan_opts() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 315
def scan_opts
  opts = {}
  opts[:limit] = @limit if @limit
  opts[:next_token] = start_key if @start
  opts[:batch_size] = @batch_size if @batch_size
  opts
end
start_key() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 296
def start_key
        hash_key_type = @start.class.attributes[@start.class.hash_key][:type] == :string ? 'S' : 'N'
  key = { :hash_key_element => { hash_key_type => @start.hash_key.to_s } }
  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).to_s } })
  end
  key
end