1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
|
# frozen_string_literal: true
module ActiveRecord
class PredicateBuilder # :nodoc:
delegate :resolve_column_aliases, to: :table
def initialize(table)
@table = table
@handlers = []
register_handler(BasicObject, BasicObjectHandler.new(self))
register_handler(Base, BaseHandler.new(self))
register_handler(Range, RangeHandler.new(self))
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
register_handler(Set, ArrayHandler.new(self))
end
def build_from_hash(attributes)
attributes = convert_dot_notation_to_hash(attributes)
expand_from_hash(attributes)
end
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
key
else
key = key.to_s
key.split(".").first if key.include?(".")
end
end.compact
end
# Define how a class is converted to Arel nodes when passed to +where+.
# The handler can be any object that responds to +call+, and will be used
# for any value that +===+ the class given. For example:
#
# MyCustomDateRange = Struct.new(:start, :end)
# handler = proc do |column, range|
# Arel::Nodes::Between.new(column,
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
# ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
def register_handler(klass, handler)
@handlers.unshift([klass, handler])
end
def build(attribute, value)
if table.type(attribute.name).force_equality?(value)
bind = build_bind_attribute(attribute.name, value)
attribute.eq(bind)
else
handler_for(value).call(attribute, value)
end
end
def build_bind_attribute(column_name, value)
attr = Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
Arel::Nodes::BindParam.new(attr)
end
protected
def expand_from_hash(attributes)
return ["1=0"] if attributes.empty?
attributes.flat_map do |key, value|
if value.is_a?(Hash) && !table.has_column?(key)
table.associated_predicate_builder(key).expand_from_hash(value)
elsif table.associated_with?(key)
# Find the foreign key when using queries such as:
# Post.where(author: author)
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
associated_table = table.associated_table(key)
if associated_table.polymorphic_association?
case value.is_a?(Array) ? value.first : value
when Base, Relation
value = [value] unless value.is_a?(Array)
klass = PolymorphicArrayValue
end
end
klass ||= AssociationQueryValue
queries = klass.new(associated_table, value).queries.map do |query|
expand_from_hash(query).reduce(&:and)
end
queries.reduce(&:or)
elsif table.aggregated_with?(key)
mapping = table.reflect_on_aggregation(key).mapping
values = value.nil? ? [nil] : Array.wrap(value)
if mapping.length == 1 || values.empty?
column_name, aggr_attr = mapping.first
values = values.map do |object|
object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
end
build(table.arel_attribute(column_name), values)
else
queries = values.map do |object|
mapping.map do |field_attr, aggregate_attr|
build(table.arel_attribute(field_attr), object.try!(aggregate_attr))
end.reduce(&:and)
end
queries.reduce(&:or)
end
else
build(table.arel_attribute(key), value)
end
end
end
private
attr_reader :table
def convert_dot_notation_to_hash(attributes)
dot_notation = attributes.select do |k, v|
k.include?(".") && !v.is_a?(Hash)
end
dot_notation.each_key do |key|
table_name, column_name = key.split(".")
value = attributes.delete(key)
attributes[table_name] ||= {}
attributes[table_name] = attributes[table_name].merge(column_name => value)
end
attributes
end
def handler_for(object)
@handlers.detect { |klass, _| klass === object }.last
end
end
end
require "active_record/relation/predicate_builder/array_handler"
require "active_record/relation/predicate_builder/base_handler"
require "active_record/relation/predicate_builder/basic_object_handler"
require "active_record/relation/predicate_builder/range_handler"
require "active_record/relation/predicate_builder/relation_handler"
require "active_record/relation/predicate_builder/association_query_value"
require "active_record/relation/predicate_builder/polymorphic_array_value"
|