Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Equality should handle NOT values with != () or NOT LIKE () #165

Closed
wants to merge 1 commit into from

6 participants

@avit

This fixes SQL syntax when composing Not nodes into binary comparison nodes and cancels double-negatives:

  • .eq(not_x) "!= x"
  • .not_eq(not_x) "= x"
  • .matches(not_x) "NOT LIKE x"
  • .does_not_match(not_x) "LIKE x"

Use case: in a Ransack search I want to use eq or not_eq to include "y != 'chunky' OR y IS NULL". (Simple != will skip NULL rows.) This currently does not work:

attr.eq_any( [Nodes::Not('chunky'), nil] )
@avit avit referenced this pull request in activerecord-hackery/ransack
Open

Handle `not_eq` with null rows #123

@ernie
Collaborator

@avit I don't think making not_eq send back different node types is the answer. Do you think this could be handled in the visitor, instead?

@avit

@ernie Sure, I could have another look at doing it that way. It seems like we could accomplish the same thing fairly easily by unpacking Not nodes in visit_Arel_Nodes_Equality & friends.

@avit

Some of the other predication methods do the same thing, e.g. .in and .not_in also return different node types based on the right-hand value. Is the reasoning for this logic going into the visitor different from those?

I suppose someone could manually build a tree with Not(Not(...)) and then the visitor would get it wrong?

@ernie
Collaborator

@avit Fair point re: in and not_in. I think that was mostly a concession to the fact that Arel is really focused on generating SQL, and since SQL has different syntax for comparison of a begin and end vs a list of values, it was deemed appropriate to handle in this way. I'm not entirely convinced this wouldn't be cleaner at the visitor level, too, but for purposes of demonstrating consistency with the existing code, you win. ;)

@avit avit Equality should handle NOT values with != () or NOT LIKE ()
Fix SQL syntax and cancel double-negatives:

* .eq(not_x)       '!= x'
* .not_eq(not_x)   '= x'
* .like(not_x)     'NOT LIKE x'
* .not_like(not_x) 'LIKE x'

(Issue #165)
17a7fd5
@avit

@ernie, please review changes, I moved the logic into the visitor. Works the same.

@bf4

bump

@ernie
Collaborator

@avit I'm not sure how I missed this somewhat odd use case in the original description. I'm not sure a Ransack requirement should change the behavior of Not in this way. Reasoning is that back in the Arel 1.x days I made a similar change but we didn't bother porting it for 2.x.

@avit

@ernie I didn't request this strictly for Ransack: I would argue that handling double-negative algebra almost certainly belongs here in Arel rather than working around it downstream.

I pointed out the "somewhat odd use case" as one example. The convoluted SQL syntax that comes out when composing & negating ActiveRecord scopes is sometimes valid ((NOT(status != 'active'))), but often it outputs broken SQL syntax even though the Arel AST should be workable. It would be nice if it worked consistently; and the output would read more simply too.

This patch is working fine for us in our application. Are there any real objections for just fixing this here? In that case let's just close this...

@ernie
Collaborator

@avit one of the guiding principles of Arel has been that it shouldn't police your SQL for correctness, so I wouldn't say that potential output of bad SQL (if what you built in the AST would map to that bad SQL) is an issue, necessarily.

I don't want to close this without giving @tenderlove a chance to weigh in, though, in case he disagrees.

@bf4

@ernie Certainly there's at least something salvageable in this PR. I'm not a maintainer, but it seems an improvement re: principle of least surprise.

@tenderlove
Owner
@avit

sad :panda:

This question is ultimately about how to rely on Arel to build the AST... I guess the question is whether Arel should know some semantics about its own nodes, or whether the downstream application needs to sniff node types to recombine the AST correctly. Consider:

scope :matching_selected, ->(other_nodes) { where my_attr.matches(other_nodes) }

If we want to negate the other_nodes, right now we can't just pass in other_nodes.not safely without checking its class and unpacking it:

scope :matching_selected, ->(other_nodes) {
  if Arel::Nodes::Not === other_nodes
    where my_attr.does_not_match(other_nodes.value)
  else
    where my_attr.matches(other_nodes)
  end
}

My personal opinion is that Arel should do it.

@avit avit referenced this pull request in activerecord-hackery/ransack
Merged

Generate a condition when value is false for boolean predicates #335

@tamird

looks like this was decided against. should we close it?

@matthewd matthewd closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 5, 2013
  1. @avit

    Equality should handle NOT values with != () or NOT LIKE ()

    avit authored
    Fix SQL syntax and cancel double-negatives:
    
    * .eq(not_x)       '!= x'
    * .not_eq(not_x)   '= x'
    * .like(not_x)     'NOT LIKE x'
    * .not_like(not_x) 'LIKE x'
    
    (Issue #165)
This page is out of date. Refresh to see the latest.
Showing with 152 additions and 11 deletions.
  1. +20 −10 lib/arel/visitors/to_sql.rb
  2. +132 −1 test/attributes/test_attribute.rb
View
30 lib/arel/visitors/to_sql.rb
@@ -432,12 +432,20 @@ def visit_Arel_Nodes_LessThan o, a
def visit_Arel_Nodes_Matches o, a
a = o.left if Arel::Attributes::Attribute === o.left
- "#{visit o.left, a} LIKE #{visit o.right, a}"
+ if Arel::Nodes::Not === o.right
+ visit Arel::Nodes::DoesNotMatch.new(o.left, o.right.expr)
+ else
+ "#{visit o.left, a} LIKE #{visit o.right, a}"
+ end
end
def visit_Arel_Nodes_DoesNotMatch o, a
a = o.left if Arel::Attributes::Attribute === o.left
- "#{visit o.left, a} NOT LIKE #{visit o.right, a}"
+ if Arel::Nodes::Not === o.right
+ visit Arel::Nodes::Matches.new(o.left, o.right.expr)
+ else
+ "#{visit o.left, a} NOT LIKE #{visit o.right, a}"
+ end
end
def visit_Arel_Nodes_JoinSource o, a
@@ -512,24 +520,26 @@ def visit_Arel_Nodes_Assignment o, a
end
def visit_Arel_Nodes_Equality o, a
- right = o.right
-
a = o.left if Arel::Attributes::Attribute === o.left
- if right.nil?
+ case o.right
+ when Arel::Nodes::Not
+ visit Arel::Nodes::NotEqual.new(o.left, o.right.expr)
+ when nil
"#{visit o.left, a} IS NULL"
else
- "#{visit o.left, a} = #{visit right, a}"
+ "#{visit o.left, a} = #{visit o.right, a}"
end
end
def visit_Arel_Nodes_NotEqual o, a
- right = o.right
-
a = o.left if Arel::Attributes::Attribute === o.left
- if right.nil?
+ case o.right
+ when Arel::Nodes::Not
+ visit Arel::Nodes::Equality.new(o.left, o.right.expr)
+ when nil
"#{visit o.left, a} IS NOT NULL"
else
- "#{visit o.left, a} != #{visit right, a}"
+ "#{visit o.left, a} != #{visit o.right, a}"
end
end
View
133 test/attributes/test_attribute.rb
@@ -26,6 +26,28 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL
}
end
+
+ describe 'with negation' do
+ it 'should generate != in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ value = Nodes::Not.new(10)
+ mgr.where relation[:id].not_eq(value)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" = 10
+ }
+ end
+
+ it 'should handle nil' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ value = Nodes::Not.new(nil)
+ mgr.where relation[:id].not_eq(value)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL
+ }
+ end
+ end
end
describe '#not_eq_any' do
@@ -42,6 +64,15 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" != 2)
}
end
+
+ it 'should handle double negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_any([Nodes::Not.new(1),2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 OR "users"."id" != 2)
+ }
+ end
end
describe '#not_eq_all' do
@@ -58,6 +89,15 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" != 2)
}
end
+
+ it 'should handle double negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_all([Nodes::Not.new(1),2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" != 2)
+ }
+ end
end
describe '#gt' do
@@ -66,7 +106,7 @@ module Attributes
relation[:id].gt(10).must_be_kind_of Nodes::GreaterThan
end
- it 'should generate >= in sql' do
+ it 'should generate > in sql' do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gt(10)
@@ -350,6 +390,26 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL
}
end
+
+ describe 'with negation' do
+ it 'should generate != in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq Nodes::Not.new(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" != 10
+ }
+ end
+
+ it 'should handle nil' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq Nodes::Not.new(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL
+ }
+ end
+ end
end
describe '#eq_any' do
@@ -374,6 +434,15 @@ module Attributes
mgr.where relation[:id].eq_any(values)
values.must_equal [1,2]
end
+
+ it 'should handle negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_any([Nodes::Not.new(1),2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" = 2)
+ }
+ end
end
describe '#eq_all' do
@@ -398,6 +467,15 @@ module Attributes
mgr.where relation[:id].eq_all(values)
values.must_equal [1,2]
end
+
+ it 'should handle negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([Nodes::Not.new(1),2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" = 2)
+ }
+ end
end
describe '#matches' do
@@ -414,6 +492,18 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%'
}
end
+
+ describe 'with negation' do
+ it 'should generate NOT LIKE in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ healthy_breakfast = Nodes::Not.new('%bacon%')
+ mgr.where relation[:name].matches(healthy_breakfast)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%'
+ }
+ end
+ end
end
describe '#matches_any' do
@@ -430,6 +520,16 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' OR "users"."name" LIKE '%bacon%')
}
end
+
+ it 'should handle negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ smooth = Nodes::Not.new('%chunky%')
+ mgr.where relation[:name].matches_any([smooth,'%bacon%'])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' OR "users"."name" LIKE '%bacon%')
+ }
+ end
end
describe '#matches_all' do
@@ -446,6 +546,16 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' AND "users"."name" LIKE '%bacon%')
}
end
+
+ it 'should handle negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ smooth = Nodes::Not.new('%chunky%')
+ mgr.where relation[:name].matches_all([smooth,'%bacon%'])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' AND "users"."name" LIKE '%bacon%')
+ }
+ end
end
describe '#does_not_match' do
@@ -462,6 +572,18 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%'
}
end
+
+ describe 'with negation' do
+ it 'should generate LIKE in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ healthy_breakfast = Nodes::Not.new('%bacon%')
+ mgr.where relation[:name].does_not_match(healthy_breakfast)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%'
+ }
+ end
+ end
end
describe '#does_not_match_any' do
@@ -616,6 +738,15 @@ module Attributes
SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
}
end
+
+ it 'should handle negation in sql' do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([Nodes::Not.new(1),2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" = 2)
+ }
+ end
end
describe '#asc' do
Something went wrong with that request. Please try again.