Skip to content

Instantly share code, notes, and snippets.

@Eric-Guo
Forked from bf4/about.md
Last active September 16, 2015 06:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Eric-Guo/4be0f2d6e6bf86d26ab7 to your computer and use it in GitHub Desktop.
Save Eric-Guo/4be0f2d6e6bf86d26ab7 to your computer and use it in GitHub Desktop.
ActiveRecord patch to add 'or' support as ActiveRecord::Relation#or
From 719978ecd8d447363cefae186f96e6f8e68d177b Mon Sep 17 00:00:00 2001
From: Benjamin Fleischer <github@benjaminfleischer.com>
Date: Wed, 29 Jul 2015 15:03:17 -0500
Subject: [PATCH 1/3] Backport ActiveRecord::Relation#or step 1/2
---
.../activerecord/active_relation_or.rb | 78 ++++++++++++++++++++++
1 file changed, 78 insertions(+)
create mode 100644 lib/core_extensions/activerecord/active_relation_or.rb
diff --git a/lib/core_extensions/activerecord/active_relation_or.rb b/lib/core_extensions/activerecord/active_relation_or.rb
new file mode 100644
index 0000000..2988beb
--- /dev/null
+++ b/lib/core_extensions/activerecord/active_relation_or.rb
@@ -0,0 +1,78 @@
+# https://github.com/rails/rails/commit/9e42cf019f2417473e7dcbfcb885709fa2709f89.patch
+# CHANGELOG.md
+# * Added the `#or` method on ActiveRecord::Relation, allowing use of the OR
+# operator to combine WHERE or HAVING clauses.
+#
+# Example:
+#
+# Post.where('id = 1').or(Post.where('id = 2'))
+# # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
+#
+# *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
+
+ActiveSupport.on_load(:active_record) do
+
+ module ActiveRecord::NullRelation
+ def or(other)
+ other.spawn
+ end
+ end
+
+ module ActiveRecord::Querying
+ delegate :or, to: :all
+ end
+
+ module ActiveRecord::QueryMethods
+
+ # Returns a new relation, which is the logical union of this relation and the one passed as an
+ # argument.
+ #
+ # The two relations must be structurally compatible: they must be scoping the same model, and
+ # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is
+ # present). Neither relation may have a +limit+, +offset+, or +uniq+ set.
+ #
+ # Post.where("id = 1").or(Post.where("id = 2"))
+ # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
+ #
+ def or(other)
+ spawn.or!(other)
+ end
+
+ def or!(other) # :nodoc:
+ unless structurally_compatible_for_or?(other)
+ raise ArgumentError, 'Relation passed to #or must be structurally compatible'
+ end
+
+ self.where_clause = self.where_clause.or(other.where_clause)
+ self.having_clause = self.having_clause.or(other.having_clause)
+
+ self
+ end
+
+ private def structurally_compatible_for_or?(other) # :nodoc:
+ (ActiveRecord::Relation::SINGLE_VALUE_METHODS - [:from]).all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
+ (ActiveRecord::Relation::MULTI_VALUE_METHODS - [:extending, :where, :having, :bind]).all? { |m| send("#{m}_values") == other.send("#{m}_values") }
+ # https://github.com/rails/rails/commit/2c46d6db4feaf4284415f2fb6ceceb1bb535f278
+ # https://github.com/rails/rails/commit/39f2c3b3ea6fac371e79c284494e3d4cfdc1e929
+ # https://github.com/rails/rails/commit/bdc5141652770fd227455681cde1f9899f55b0b9
+ # (ActiveRecord::Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
+ end
+
+ end
+
+ class ActiveRecord::Relation::WhereClause
+
+ def or(other)
+ if empty?
+ other
+ elsif other.empty?
+ self
+ else
+ WhereClause.new(
+ [ast.or(other.ast)],
+ binds + other.binds
+ )
+ end
+ end
+ end
+end
From 98eb1a518c293be0592bc4f415f5e2fad7913d07 Mon Sep 17 00:00:00 2001
From: Benjamin Fleischer <github@benjaminfleischer.com>
Date: Wed, 29 Jul 2015 15:03:23 -0500
Subject: [PATCH 2/3] Backport ActiveRecord::Relation#or Step 2/2
Role.where(id: 1).or(Role.where(id: 2)).to_sql
=> "SELECT `roles`.* FROM `roles` WHERE (`roles`.`id` = 1 OR `roles`.`id` = 2)"
---
.../activerecord/active_relation_or.rb | 202 ++++++++++++++++++++-
1 file changed, 197 insertions(+), 5 deletions(-)
diff --git a/lib/core_extensions/activerecord/active_relation_or.rb b/lib/core_extensions/activerecord/active_relation_or.rb
index 2988beb..fe05067 100644
--- a/lib/core_extensions/activerecord/active_relation_or.rb
+++ b/lib/core_extensions/activerecord/active_relation_or.rb
@@ -24,6 +24,38 @@ module ActiveRecord::Querying
module ActiveRecord::QueryMethods
+ CLAUSE_METHODS = [:where, :having]
+
+ CLAUSE_METHODS.each do |name|
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}_clause # def where_clause
+ @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause
+ end # end
+ #
+ def #{name}_clause=(value) # def where_clause=(value)
+ raise ImmutableRelation if @loaded
+ check_cached_relation # assert_mutability!
+ @values[:#{name}] = value # @values[:where] = value
+ end # end
+ CODE
+ end
+
+ def where_values
+ where_clause.predicates
+ end
+
+ def where_values=(values)
+ self.where_clause = ActiveRecord::Relation::WhereClause.new(values || [], where_clause.binds)
+ end
+
+ def bind_values
+ where_clause.binds
+ end
+
+ def bind_values=(values)
+ self.where_clause = ActiveRecord::Relation::WhereClause.new(where_clause.predicates, values || [])
+ end
+
# Returns a new relation, which is the logical union of this relation and the one passed as an
# argument.
#
@@ -52,15 +84,51 @@ def or!(other) # :nodoc:
private def structurally_compatible_for_or?(other) # :nodoc:
(ActiveRecord::Relation::SINGLE_VALUE_METHODS - [:from]).all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
(ActiveRecord::Relation::MULTI_VALUE_METHODS - [:extending, :where, :having, :bind]).all? { |m| send("#{m}_values") == other.send("#{m}_values") }
- # https://github.com/rails/rails/commit/2c46d6db4feaf4284415f2fb6ceceb1bb535f278
- # https://github.com/rails/rails/commit/39f2c3b3ea6fac371e79c284494e3d4cfdc1e929
- # https://github.com/rails/rails/commit/bdc5141652770fd227455681cde1f9899f55b0b9
- # (ActiveRecord::Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
+ # https://github.com/rails/rails/commit/2c46d6db4feaf4284415f2fb6ceceb1bb535f278
+ # https://github.com/rails/rails/commit/39f2c3b3ea6fac371e79c284494e3d4cfdc1e929
+ # https://github.com/rails/rails/commit/bdc5141652770fd227455681cde1f9899f55b0b9
+ # (ActiveRecord::Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
end
+ private
+
+ def new_where_clause
+ ActiveRecord::Relation::WhereClause.empty
+ end
+ alias new_having_clause new_where_clause
end
class ActiveRecord::Relation::WhereClause
+ # https://github.com/rails/rails/commit/d26dd00854c783bcb1249168bb3f4adf9f99be6c
+ attr_reader :binds, :predicates
+
+ delegate :any?, :empty?, to: :predicates
+
+ def initialize(predicates, binds)
+ @predicates = predicates
+ @binds = binds
+ end
+
+ def +(other)
+ ActiveRecord::Relation::WhereClause.new(
+ predicates + other.predicates,
+ binds + other.binds,
+ )
+ end
+
+ def merge(other)
+ ActiveRecord::Relation::WhereClause.new(
+ predicates_unreferenced_by(other) + other.predicates,
+ non_conflicting_binds(other) + other.binds,
+ )
+ end
+
+ def except(*columns)
+ ActiveRecord::Relation::WhereClause.new(
+ predicates_except(columns),
+ binds_except(columns),
+ )
+ end
def or(other)
if empty?
@@ -68,11 +136,135 @@ def or(other)
elsif other.empty?
self
else
- WhereClause.new(
+ ActiveRecord::Relation::WhereClause.new(
[ast.or(other.ast)],
binds + other.binds
)
end
end
+
+ def to_h(table_name = nil)
+ equalities = predicates.grep(Arel::Nodes::Equality)
+ if table_name
+ equalities = equalities.select do |node|
+ node.left.relation.name == table_name
+ end
+ end
+
+ binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h
+
+ equalities.map { |node|
+ name = node.left.name
+ [name, binds.fetch(name.to_s) {
+ case node.right
+ when Array then node.right.map(&:val)
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
+ node.right.val
+ end
+ }]
+ }.to_h
+ end
+
+ def ast
+ Arel::Nodes::And.new(predicates_with_wrapped_sql_literals)
+ end
+
+ def ==(other)
+ other.is_a?(ActiveRecord::Relation::WhereClause) &&
+ predicates == other.predicates &&
+ binds == other.binds
+ end
+
+ def invert
+ ActiveRecord::Relation::WhereClause.new(inverted_predicates, binds)
+ end
+
+ def self.empty
+ new([], [])
+ end
+
+ protected
+
+ def referenced_columns
+ @referenced_columns ||= begin
+ equality_nodes = predicates.select { |n| equality_node?(n) }
+ Set.new(equality_nodes, &:left)
+ end
+ end
+
+ private
+
+ def predicates_unreferenced_by(other)
+ predicates.reject do |n|
+ equality_node?(n) && other.referenced_columns.include?(n.left)
+ end
+ end
+
+ def equality_node?(node)
+ node.respond_to?(:operator) && node.operator == :==
+ end
+
+ def non_conflicting_binds(other)
+ conflicts = referenced_columns & other.referenced_columns
+ conflicts.map! { |node| node.name.to_s }
+ binds.reject { |attr| conflicts.include?(attr.name) }
+ end
+
+ def inverted_predicates
+ predicates.map { |node| invert_predicate(node) }
+ end
+
+ def invert_predicate(node)
+ case node
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(node.left, node.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(node.left, node.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
+ else
+ Arel::Nodes::Not.new(node)
+ end
+ end
+
+ def predicates_except(columns)
+ predicates.reject do |node|
+ case node
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
+ subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
+ columns.include?(subrelation.name.to_s)
+ end
+ end
+ end
+
+ def binds_except(columns)
+ binds.reject do |attr|
+ columns.include?(attr.name)
+ end
+ end
+
+ def predicates_with_wrapped_sql_literals
+ non_empty_predicates.map do |node|
+ if Arel::Nodes::Equality === node
+ node
+ else
+ wrap_sql_literal(node)
+ end
+ end
+ end
+
+ def non_empty_predicates
+ predicates - ['']
+ end
+
+ def wrap_sql_literal(node)
+ if ::String === node
+ node = Arel.sql(node)
+ end
+ Arel::Nodes::Grouping.new(node)
+ end
+
end
end
From 30c16862f3920a612df3ecc9b6c9e23e6c358355 Mon Sep 17 00:00:00 2001
From: Benjamin Fleischer <github@benjaminfleischer.com>
Date: Wed, 29 Jul 2015 15:09:04 -0500
Subject: [PATCH 3/3] Self-expire ActiveRecord::Relation#or patch
---
lib/core_extensions/activerecord/active_relation_or.rb | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lib/core_extensions/activerecord/active_relation_or.rb b/lib/core_extensions/activerecord/active_relation_or.rb
index fe05067..ae6dd8f 100644
--- a/lib/core_extensions/activerecord/active_relation_or.rb
+++ b/lib/core_extensions/activerecord/active_relation_or.rb
@@ -1,3 +1,6 @@
+abort "Congrats for being on Rails 5. Now please remove this patch" if Rails::VERSION::MAJOR > 4
+# Tested on Rails Rails 4.2.3
+warn "Patching ActiveRecord::Relation#or. This might blow up" if Rails.version < '4.2.3'
# https://github.com/rails/rails/commit/9e42cf019f2417473e7dcbfcb885709fa2709f89.patch
# CHANGELOG.md
# * Added the `#or` method on ActiveRecord::Relation, allowing use of the OR
abort "Congrats for being on Rails 5. Now please remove this patch" if Rails::VERSION::MAJOR > 4
# Tested on Rails Rails 4.2.3
warn "Patching ActiveRecord::Relation#or. This might blow up" if Rails.version < '4.2.3'
# https://github.com/rails/rails/commit/9e42cf019f2417473e7dcbfcb885709fa2709f89.patch
# CHANGELOG.md
# * Added the `#or` method on ActiveRecord::Relation, allowing use of the OR
# operator to combine WHERE or HAVING clauses.
#
# Example:
#
# Post.where('id = 1').or(Post.where('id = 2'))
# # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
#
# *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
ActiveSupport.on_load(:active_record) do
module ActiveRecord::NullRelation
def or(other)
other.spawn
end
end
module ActiveRecord::Querying
delegate :or, to: :all
end
module ActiveRecord::QueryMethods
CLAUSE_METHODS = [:where, :having]
CLAUSE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_clause # def where_clause
@values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause
end # end
#
def #{name}_clause=(value) # def where_clause=(value)
raise ImmutableRelation if @loaded
check_cached_relation # assert_mutability!
@values[:#{name}] = value # @values[:where] = value
end # end
CODE
end
def where_values
where_clause.predicates
end
def where_values=(values)
self.where_clause = ActiveRecord::Relation::WhereClause.new(values || [], where_clause.binds)
end
def bind_values
where_clause.binds
end
def bind_values=(values)
self.where_clause = ActiveRecord::Relation::WhereClause.new(where_clause.predicates, values || [])
end
# Returns a new relation, which is the logical union of this relation and the one passed as an
# argument.
#
# The two relations must be structurally compatible: they must be scoping the same model, and
# they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is
# present). Neither relation may have a +limit+, +offset+, or +uniq+ set.
#
# Post.where("id = 1").or(Post.where("id = 2"))
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
#
def or(other)
spawn.or!(other)
end
def or!(other) # :nodoc:
unless structurally_compatible_for_or?(other)
raise ArgumentError, 'Relation passed to #or must be structurally compatible'
end
self.where_clause = self.where_clause.or(other.where_clause)
self.having_clause = self.having_clause.or(other.having_clause)
self
end
private def structurally_compatible_for_or?(other) # :nodoc:
(ActiveRecord::Relation::SINGLE_VALUE_METHODS - [:from]).all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
(ActiveRecord::Relation::MULTI_VALUE_METHODS - [:extending, :where, :having, :bind]).all? { |m| send("#{m}_values") == other.send("#{m}_values") }
# https://github.com/rails/rails/commit/2c46d6db4feaf4284415f2fb6ceceb1bb535f278
# https://github.com/rails/rails/commit/39f2c3b3ea6fac371e79c284494e3d4cfdc1e929
# https://github.com/rails/rails/commit/bdc5141652770fd227455681cde1f9899f55b0b9
# (ActiveRecord::Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
end
private
def new_where_clause
ActiveRecord::Relation::WhereClause.empty
end
alias new_having_clause new_where_clause
end
class ActiveRecord::Relation::WhereClause
# https://github.com/rails/rails/commit/d26dd00854c783bcb1249168bb3f4adf9f99be6c
attr_reader :binds, :predicates
delegate :any?, :empty?, to: :predicates
def initialize(predicates, binds)
@predicates = predicates
@binds = binds
end
def +(other)
ActiveRecord::Relation::WhereClause.new(
predicates + other.predicates,
binds + other.binds,
)
end
def merge(other)
ActiveRecord::Relation::WhereClause.new(
predicates_unreferenced_by(other) + other.predicates,
non_conflicting_binds(other) + other.binds,
)
end
def except(*columns)
ActiveRecord::Relation::WhereClause.new(
predicates_except(columns),
binds_except(columns),
)
end
def or(other)
if empty?
other
elsif other.empty?
self
else
ActiveRecord::Relation::WhereClause.new(
[ast.or(other.ast)],
binds + other.binds
)
end
end
def to_h(table_name = nil)
equalities = predicates.grep(Arel::Nodes::Equality)
if table_name
equalities = equalities.select do |node|
node.left.relation.name == table_name
end
end
binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h
equalities.map { |node|
name = node.left.name
[name, binds.fetch(name.to_s) {
case node.right
when Array then node.right.map(&:val)
when Arel::Nodes::Casted, Arel::Nodes::Quoted
node.right.val
end
}]
}.to_h
end
def ast
Arel::Nodes::And.new(predicates_with_wrapped_sql_literals)
end
def ==(other)
other.is_a?(ActiveRecord::Relation::WhereClause) &&
predicates == other.predicates &&
binds == other.binds
end
def invert
ActiveRecord::Relation::WhereClause.new(inverted_predicates, binds)
end
def self.empty
new([], [])
end
protected
def referenced_columns
@referenced_columns ||= begin
equality_nodes = predicates.select { |n| equality_node?(n) }
Set.new(equality_nodes, &:left)
end
end
private
def predicates_unreferenced_by(other)
predicates.reject do |n|
equality_node?(n) && other.referenced_columns.include?(n.left)
end
end
def equality_node?(node)
node.respond_to?(:operator) && node.operator == :==
end
def non_conflicting_binds(other)
conflicts = referenced_columns & other.referenced_columns
conflicts.map! { |node| node.name.to_s }
binds.reject { |attr| conflicts.include?(attr.name) }
end
def inverted_predicates
predicates.map { |node| invert_predicate(node) }
end
def invert_predicate(node)
case node
when NilClass
raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
when Arel::Nodes::In
Arel::Nodes::NotIn.new(node.left, node.right)
when Arel::Nodes::Equality
Arel::Nodes::NotEqual.new(node.left, node.right)
when String
Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
else
Arel::Nodes::Not.new(node)
end
end
def predicates_except(columns)
predicates.reject do |node|
case node
when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
columns.include?(subrelation.name.to_s)
end
end
end
def binds_except(columns)
binds.reject do |attr|
columns.include?(attr.name)
end
end
def predicates_with_wrapped_sql_literals
non_empty_predicates.map do |node|
if Arel::Nodes::Equality === node
node
else
wrap_sql_literal(node)
end
end
end
def non_empty_predicates
predicates - ['']
end
def wrap_sql_literal(node)
if ::String === node
node = Arel.sql(node)
end
Arel::Nodes::Grouping.new(node)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment