Skip to content

Commit

Permalink
Support providing arbitrary SQL strings
Browse files Browse the repository at this point in the history
This commit provides a low level extension point in the
hash structure to provide a lambda that returns the section of the query that
is being asked for.

This allows people to query for any DB types or operated that aren't directly
supported in SearchCop, and SearchCop doesn't have to support them.

It's a pretty low level API in that it expects you to know about what nodes and
visitors are, but this seems like a starting point that gets users past being
stuck, and could be built on in the future.

One of those things that I think could be useful is providing this same
structure, but as one of the `options` instead and that would be used every time
you query for that said attribute. The downside of the method proposed in this
PR is that you have to use the hash parser, and inline your SQL there and every
other place you want to use it. Providing it as a default option would allow
that.

Related to mrkamel#14, mrkamel#28 and mrkamel#31.
  • Loading branch information
jakecraige committed Mar 24, 2017
1 parent 743e230 commit df9fa0a
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Book.search(:author => "Rowling", :title => "Harry Potter")
Book.search(:or => [{:author => "Rowling"}, {:author => "Tolkien"}])
Book.search(:and => [{:price => {:gt => 10}}, {:not => {:stock => 0}}, :or => [{:title => "Potter"}, {:author => "Rowling"}]])
Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}])
Book.search({:title => {:sql => ->(node, visitor) { "#{visitor.visit node.left} LIKE '%Rowl%'" }}})
# ...
```

Expand Down
8 changes: 6 additions & 2 deletions lib/search_cop/hash_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ def parse_attribute(key, value)
collection = SearchCopGrammar::Attributes::Collection.new(query_info, key.to_s)

if value.is_a?(Hash)
raise(SearchCop::ParseError, "Unknown operator #{value.keys.first}") unless [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].include?(value.keys.first)
raise(SearchCop::ParseError, "Unknown operator #{value.keys.first}") unless [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq, :sql].include?(value.keys.first)

collection.send value.keys.first, value.values.first.to_s
if value.keys.first == :sql
collection.send value.keys.first, value.values.first
else
collection.send value.keys.first, value.values.first.to_s
end
else
collection.send :matches, value.to_s
end
Expand Down
4 changes: 4 additions & 0 deletions lib/search_cop/visitors/visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ def visit_SearchCopGrammar_Nodes_Not(node)
"NOT (#{visit node.object})"
end

def visit_SearchCopGrammar_Nodes_Sql(node)
node.right.call(node, self)
end

def quote_table_name(name)
connection.quote_table_name name
end
Expand Down
6 changes: 6 additions & 0 deletions lib/search_cop_grammar/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ def hash
end
end

def sql(value)
attributes.collect! do |attribute|
SearchCopGrammar::Nodes::Sql.new(attribute, value)
end.inject(:or)
end

def matches(value)
if fulltext?
SearchCopGrammar::Nodes::MatchesFulltext.new self, value.to_s
Expand Down
1 change: 1 addition & 0 deletions lib/search_cop_grammar/nodes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class GreaterThanOrEqual < Binary; end
class LessThan < Binary; end
class LessThanOrEqual < Binary; end
class Matches < Binary; end
class Sql < Binary; end

class Not
include Base
Expand Down
13 changes: 13 additions & 0 deletions test/hash_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,18 @@ def test_lteq
assert_includes results, expected
refute_includes results, rejected
end

def test_sql
expected = create(:product, :title => "Expected")
rejected = create(:product, :title => "Rejected")

sql_generator = lambda do |node, visitor|
"#{visitor.visit node.left} = 'Expected'"
end
results = Product.search(:title => { :sql => sql_generator })

assert_includes results, expected
refute_includes results, rejected
end
end

9 changes: 9 additions & 0 deletions test/visitor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,14 @@ def test_fulltext_or
assert_equal("(MATCH(`products`.`title`) AGAINST('(Query1) (Query2)' IN BOOLEAN MODE))", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)) if ENV["DATABASE"] == "mysql"
assert_equal("(to_tsvector('english', COALESCE(\"products\".\"title\", '')) @@ to_tsquery('english', '(''Query1'') | (''Query2'')'))", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)) if ENV["DATABASE"] == "postgres"
end

def test_sql
sql_generator = ->(node, visitor) do
"#{visitor.visit node.left} = SQL STRING"
end
node = SearchCopGrammar::Attributes::Collection.new(SearchCop::QueryInfo.new(Product, Product.search_scopes[:search]), "title").sql(sql_generator).optimize!

assert_equal "#{quote_table_name "products"}.#{quote_column_name "title"} = SQL STRING", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)
end
end

0 comments on commit df9fa0a

Please sign in to comment.