Skip to content

Instantly share code, notes, and snippets.

@estum
Created October 10, 2020 13:09
Show Gist options
  • Save estum/4ccec1dcb054f91bcc562a6a652ba67f to your computer and use it in GitHub Desktop.
Save estum/4ccec1dcb054f91bcc562a6a652ba67f to your computer and use it in GitHub Desktop.
Fix the array handling on ActiveRecord's prepared statements
# frozen_string_literal: true
# Before (creates prepared statement for each varying size of given array):
#
# $ Post.where(id: [uuid1, uuid2])
# => Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" IN ($1, $2) [["id", "a830f21d-a27b-4bde-8c05-6e5dd088712e"], ["id","531ee026-d60d-4a59-a9a5-d44c44578a98}"]]
#
# After (the only one statement for varying size of given array):
#
# $ Post.where(id: [uuid1, uuid2])
# => Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ANY($1) [["id", "{a830f21d-a27b-4bde-8c05-6e5dd088712e,531ee026-d60d-4a59-a9a5-d44c44578a98}"]]
module PredicateBuilderArrayFix
module BuilderHook
def build_bind_attribute(column_name, value)
if value.is_a?(Array)
column_type = table.type(column_name.to_sym).type
array_type = Type.lookup(column_type, array: true)
attr = Relation::QueryAttribute.new(column_name, value, array_type)
Arel::Nodes::BindParam.new(attr)
else
super
end
end
end
module HandlerHook
def call(attribute, value)
return attribute.in([]) if value.empty?
values = value.map { |x| x.is_a?(Base) ? x.id : x }
nils, values = values.partition(&:nil?)
ranges, values = values.partition { |v| v.is_a?(Range) }
values_predicate =
case values.length
when 0 then NullPredicate
when 1 then predicate_builder.build(attribute, values.first)
else
values = predicate_builder.build_bind_attribute(attribute.name, values)
attribute.eq(Arel::Nodes::NamedFunction.new("ANY", [values]))
end
unless nils.empty?
values_predicate = values_predicate.or(predicate_builder.build(attribute, nil))
end
array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) }
array_predicates.unshift(values_predicate)
array_predicates.inject(&:or)
end
end
end
ActiveSupport.on_load(:active_record) do
ActiveRecord::PredicateBuilder.prepend(PredicateBuilderArrayFix::BuilderHook)
ActiveRecord::PredicateBuilder::ArrayHandler.prepend(PredicateBuilderArrayFix::HandlerHook)
end
@estum
Copy link
Author

estum commented Oct 10, 2020

Elegantly fixes rails/rails#33702

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment