class FrozenRecord::Scope

Constants

ARRAY_INTERSECTION
DISALLOWED_ARRAY_METHODS

Public Class Methods

new(klass) click to toggle source
# File lib/frozen_record/scope.rb, line 27
def initialize(klass)
  @klass = klass
  @where_values = []
  @where_not_values = []
  @order_values = []
  @limit = nil
  @offset = nil
end

Public Instance Methods

==(other) click to toggle source
# File lib/frozen_record/scope.rb, line 137
def ==(other)
  self.class === other &&
  comparable_attributes == other.comparable_attributes
end
average(attribute) click to toggle source
# File lib/frozen_record/scope.rb, line 89
def average(attribute)
  pluck(attribute).sum.to_f / count
end
exists?() click to toggle source
# File lib/frozen_record/scope.rb, line 101
def exists?
  !empty?
end
find(id) click to toggle source
# File lib/frozen_record/scope.rb, line 40
def find(id)
  raise RecordNotFound, "Can't lookup record without ID" unless id

  scope = self
  if @limit || @offset
    scope = limit(nil).offset(nil)
  end
  scope.find_by_id(id) or raise RecordNotFound, "Couldn't find a record with ID = #{id.inspect}"
end
find_by(criterias) click to toggle source
# File lib/frozen_record/scope.rb, line 50
def find_by(criterias)
  where(criterias).first
end
find_by!(criterias) click to toggle source
# File lib/frozen_record/scope.rb, line 54
def find_by!(criterias)
  where(criterias).first!
end
find_by_id(id) click to toggle source
# File lib/frozen_record/scope.rb, line 36
def find_by_id(id)
  find_by(@klass.primary_key => id)
end
first!() click to toggle source
# File lib/frozen_record/scope.rb, line 58
def first!
  first or raise RecordNotFound, "No record matched"
end
hash() click to toggle source
# File lib/frozen_record/scope.rb, line 133
def hash
  comparable_attributes.hash
end
ids() click to toggle source
# File lib/frozen_record/scope.rb, line 81
def ids
  pluck(primary_key)
end
last!() click to toggle source
# File lib/frozen_record/scope.rb, line 62
def last!
  last or raise RecordNotFound, "No record matched"
end
limit(amount) click to toggle source
# File lib/frozen_record/scope.rb, line 121
def limit(amount)
  spawn.limit!(amount)
end
maximum(attribute) click to toggle source
# File lib/frozen_record/scope.rb, line 97
def maximum(attribute)
  pluck(attribute).max
end
minimum(attribute) click to toggle source
# File lib/frozen_record/scope.rb, line 93
def minimum(attribute)
  pluck(attribute).min
end
offset(amount) click to toggle source
# File lib/frozen_record/scope.rb, line 125
def offset(amount)
  spawn.offset!(amount)
end
order(*ordering) click to toggle source
# File lib/frozen_record/scope.rb, line 117
def order(*ordering)
  spawn.order!(*ordering)
end
pluck(*attributes) click to toggle source
# File lib/frozen_record/scope.rb, line 70
def pluck(*attributes)
  case attributes.length
  when 1
    to_a.map(&attributes.first.to_sym)
  when 0
    raise NotImplementedError, '`.pluck` without arguments is not supported yet'
  else
    to_a.map { |r| attributes.map { |a| r[a] }}
  end
end
respond_to_missing?(method_name, *) click to toggle source
Calls superclass method
# File lib/frozen_record/scope.rb, line 129
def respond_to_missing?(method_name, *)
  array_delegable?(method_name) || @klass.respond_to?(method_name) || super
end
sum(attribute) click to toggle source
# File lib/frozen_record/scope.rb, line 85
def sum(attribute)
  pluck(attribute).sum
end
to_a() click to toggle source
# File lib/frozen_record/scope.rb, line 66
def to_a
  query_results
end
where(criterias = :chain) click to toggle source
# File lib/frozen_record/scope.rb, line 105
def where(criterias = :chain)
  if criterias == :chain
    WhereChain.new(self)
  else
    spawn.where!(criterias)
  end
end
where_not(criterias) click to toggle source
# File lib/frozen_record/scope.rb, line 113
def where_not(criterias)
  spawn.where_not!(criterias)
end

Protected Instance Methods

array_delegable?(method) click to toggle source
# File lib/frozen_record/scope.rb, line 257
def array_delegable?(method)
  Array.method_defined?(method) && !DISALLOWED_ARRAY_METHODS.include?(method)
end
clear_cache!() click to toggle source
# File lib/frozen_record/scope.rb, line 166
def clear_cache!
  @comparable_attributes = nil
  @results = nil
  @matches = nil
  self
end
comparable_attributes() click to toggle source
# File lib/frozen_record/scope.rb, line 144
def comparable_attributes
  @comparable_attributes ||= {
    klass: @klass,
    where_values: @where_values.uniq.sort,
    where_not_values: @where_not_values.uniq.sort,
    order_values: @order_values.uniq,
    limit: @limit,
    offset: @offset,
  }
end
compare(record_a, record_b) click to toggle source
# File lib/frozen_record/scope.rb, line 236
def compare(record_a, record_b)
  @order_values.each do |attr, order|
    a_value, b_value = record_a.send(attr), record_b.send(attr)
    cmp = a_value <=> b_value
    next if cmp == 0
    return order == :asc ? cmp : -cmp
  end
  0
end
limit!(amount) click to toggle source
# File lib/frozen_record/scope.rb, line 278
def limit!(amount)
  @limit = amount
  self
end
matching_records() click to toggle source
# File lib/frozen_record/scope.rb, line 177
def matching_records
  sort_records(select_records(@klass.load_records))
end
method_missing(method_name, *args, &block) click to toggle source
Calls superclass method
# File lib/frozen_record/scope.rb, line 246
def method_missing(method_name, *args, &block)
  if array_delegable?(method_name)
    to_a.public_send(method_name, *args, &block)
  elsif @klass.respond_to?(method_name)
    scoping { @klass.public_send(method_name, *args, &block) }
  else
    super
  end
end
offset!(amount) click to toggle source
# File lib/frozen_record/scope.rb, line 283
def offset!(amount)
  @offset = amount
  self
end
order!(*ordering) click to toggle source
# File lib/frozen_record/scope.rb, line 271
def order!(*ordering)
  @order_values += ordering.map do |order|
    order.respond_to?(:to_a) ? order.to_a : [[order, :asc]]
  end.flatten(1)
  self
end
query_results() click to toggle source
# File lib/frozen_record/scope.rb, line 173
def query_results
  slice_records(matching_records)
end
scoping() { || ... } click to toggle source
# File lib/frozen_record/scope.rb, line 155
def scoping
  previous, @klass.current_scope = @klass.current_scope, self
  yield
ensure
  @klass.current_scope = previous
end
select_records(records) click to toggle source
# File lib/frozen_record/scope.rb, line 183
def select_records(records)
  return records if @where_values.empty? && @where_not_values.empty?

  indices = @klass.index_definitions
  indexed_where_values, unindexed_where_values = @where_values.partition { |criteria| indices.key?(criteria.first) }

  unless indexed_where_values.empty?
    usable_indexes = indexed_where_values.map { |(attribute, value)| [attribute, value, indices[attribute].query(value)] }
    usable_indexes.sort_by! { |r| r[2].size }
    records = usable_indexes.shift.last

    # If the index is 5 times bigger that the current set of records it's not worth doing an array intersection.
    # The value is somewhat arbitrary and could be adjusted.
    useless_indexes, usable_indexes = usable_indexes.partition { |_, _, indexed_records| indexed_records.size > records.size * 5 }
    unindexed_where_values += useless_indexes.map { |a| a.first(2) }

    unless usable_indexes.empty?
      if ARRAY_INTERSECTION
        records = records.intersection(*usable_indexes.map(&:last))
      else
        usable_indexes.each do |_, _, indexed_records|
          records &= indexed_records
        end
      end
    end
  end

  if FrozenRecord.enforce_max_records_scan && @klass.max_records_scan && records.size > @klass.max_records_scan
    raise SlowQuery, "Scanning #{records.size} records is too slow, the allowed maximum is #{@klass.max_records_scan}. Try to find a better index or consider an alternative storage"
  end

  records.select do |record|
    unindexed_where_values.all? { |attr, matcher| matcher.match?(record[attr]) } &&
    !@where_not_values.any? { |attr, matcher| matcher.match?(record[attr]) }
  end
end
slice_records(records) click to toggle source
# File lib/frozen_record/scope.rb, line 228
def slice_records(records)
  return records unless @limit || @offset

  first = @offset || 0
  last = first + (@limit || records.length)
  records[first...last] || []
end
sort_records(records) click to toggle source
# File lib/frozen_record/scope.rb, line 220
def sort_records(records)
  return records if @order_values.empty?

  records.sort do |record_a, record_b|
    compare(record_a, record_b)
  end
end
spawn() click to toggle source
# File lib/frozen_record/scope.rb, line 162
def spawn
  clone.clear_cache!
end
where!(criterias) click to toggle source
# File lib/frozen_record/scope.rb, line 261
def where!(criterias)
  @where_values += criterias.map { |k, v| [k.to_s, Matcher.for(v)] }
  self
end
where_not!(criterias) click to toggle source
# File lib/frozen_record/scope.rb, line 266
def where_not!(criterias)
  @where_not_values += criterias.map { |k, v| [k.to_s, Matcher.for(v)] }
  self
end