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() 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
  records
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 136
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 52
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 132
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 110
def first
  limit(1).first
end
scan_index_forward(scan_index_forward) click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 124
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] = 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 181
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 219
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 270
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 289
def query_opts
  opts = {}
  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
# File lib/dynamoid/criteria/chain.rb, line 274
def range?
  return false unless source.range_key
  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 234
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 253
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 [Array] an array of the found records.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 147
def records
  if range?
    records_with_range
  elsif index
    records_with_index
  else
    records_without_index
  end
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 [Array] an array of the found records.

@since 0.2.0

# File lib/dynamoid/criteria/chain.rb, line 162
def records_with_index
  ids = ids_from_index
  if ids.nil? || ids.empty?
    []
  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
    Array(source.find(ids, consistent_opts))
  end
end
records_with_range() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 192
def records_with_range
  Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.from_database(hash) }
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 [Array] an array of the found records.

@since 0.2.0

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

  Dynamoid::Adapter.scan(source.table_name, query, scan_opts).collect {|hash| source.from_database(hash) }
end
scan_opts() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 297
def scan_opts
  opts = {}
  opts[:limit] = @limit if @limit
  opts[:next_token] = start_key if @start
  opts
end
start_key() click to toggle source
# File lib/dynamoid/criteria/chain.rb, line 279
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