|
From 719978ecd8d447363cefae186f96e6f8e68d177b Mon Sep 17 00:00:00 2001 |
|
From: Benjamin Fleischer <[email protected]> |
|
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 <[email protected]> |
|
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 <[email protected]> |
|
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 |
(added Eric-Guo/where-or#2)