Skip to content

Commit

Permalink
[feature:lib] Add support for subquery grouping (#59)
Browse files Browse the repository at this point in the history
* Add support for subquery grouping

* Fix grammar in README
  • Loading branch information
eonu authored Apr 24, 2019
1 parent cdaf3a3 commit d38cbeb
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 5 deletions.
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,39 @@ q.and_not
q.category('math.NA', 'math.CO', connective: :or)
```

#### Grouping subqueries

Sometimes you'll have a query that requires nested or grouped logic, using parentheses. This can be done using the `Arx::Query#group` method.

This method accepts a block and basically parenthesises the result of whichever methods were called within the block.

For example, this will allow the last query from the previous section to be written as:

```ruby
# Papers authored by "Eleonora Andreotti" in neither the "Numerical Analysis" (math.NA) or "Combinatorics (math.CO)" categories.
q = Arx::Query.new
q.author('Eleonora Andreotti')
q.and_not
q.group do
q.category('math.NA').or.category('math.CO')
end
```

Another more complicated example with two grouped subqueries:

```ruby
# Papers whose title contains "Buchi Automata", either authored by "Tomáš Babiak", or in the "Formal Languages and Automata Theory (cs.FL)" category and not the "Computational Complexity (cs.CC)" category.
q = Arx::Query.new
q.title('Buchi Automata')
q.group do
q.author('Tomáš Babiak')
q.or
q.group do
q.category('cs.FL').and_not.category('cs.CC')
end
end
```

### Running search queries

Search queries can be executed with the `Arx()` method (alias of `Arx.search`). This method contains the same parameters as the `Arx::Query` initializer - including the list of IDs.
Expand Down Expand Up @@ -371,8 +404,6 @@ A large portion of this library is based on the brilliant work done by [Scholast

Arx was created mostly due to the seemingly inactive nature of Scholastica's repository. Additionally, it would have been infeasible to contribute such large changes to an already well-established gem, especially since https://scholasticahq.com/ appears to be dependent upon this gem.

And thus, Arx was born.

---

Nevertheless, a special thanks goes out to Scholastica for providing the influence for Arx.
28 changes: 25 additions & 3 deletions lib/arx/query/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,20 @@ def initialize(*ids, sort_by: :relevance, sort_order: :descending)
end
end

# Creates a nested subquery (grouped statements with parentheses).
#
# @return [self]
def group
add_connective :and unless end_with_connective?
@query << (search_query? ? '+' : "&#{PARAMS[:search_query]}=")

@query << CGI.escape('(')
yield
@query << CGI.escape(')')

self
end

# Returns the query string.
#
# @return [String]
Expand All @@ -189,7 +203,7 @@ def to_s
# @return [self]
def add_connective(connective)
if search_query?
@query << "+#{CONNECTIVES[connective]}" unless end_with_connective?
@query << "+#{CONNECTIVES[connective]}" unless end_with_connective? || start_of_group?
end
self
end
Expand All @@ -198,9 +212,10 @@ def add_connective(connective)
#
# @param subquery [String] The subquery to add.
def add_subquery(subquery)
add_connective :and unless end_with_connective?

if search_query?
add_connective :and unless end_with_connective?
@query << "+#{subquery}"
@query << (start_of_group? ? "#{subquery}" : "+#{subquery}")
else
@query << "&#{PARAMS[:search_query]}=#{subquery}"
end
Expand All @@ -222,6 +237,13 @@ def end_with_connective?
CONNECTIVES.values.any? &@query.method(:end_with?)
end

# Whether the query string ends in a start-of-group character '('.
#
# @return [Boolean]
def start_of_group?
@query.end_with? CGI.escape('(')
end

# Parenthesizes a string with CGI-escaped parentheses.
#
# @param string [String] The string to parenthesize.
Expand Down
109 changes: 109 additions & 0 deletions spec/arx/query/query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,115 @@
end
end

context '#group' do
subject { Query }

context 'with no search query' do
it { expect(subject.new.group {}.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=%28%29' }
it do
query = subject.new.tap do |q|
q.group { q.title 'Buchi automata' }
end
expect(query.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=%28ti:%22Buchi+automata%22%29'
end
it do
query = subject.new.tap do |q|
q.group { q.group { q.group {} } }
end
expect(query.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=%28+%28+%28%29%29%29'
end
end
context 'with no block' do
it { expect{ subject.new.title('').group }.to raise_error LocalJumpError }
end
context 'with no search query and no block' do
it { expect{ subject.new.group }.to raise_error LocalJumpError }
end
context 'with a search query and block' do
it do
query = subject.new.tap do |q|
q.title 'Buchi automata'
q.group do
q.category 'cs.FL'
q.and_not
q.author 'Tomáš Babiak'
end
end
expect(query.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=ti:%22Buchi+automata%22+AND+%28cat:%22cs.FL%22+ANDNOT+au:%22Tom%C3%A1%C5%A1+Babiak%22%29'
end
it do
query = subject.new.tap do |q|
q.title 'Buchi automata'
q.group do
q.category 'cs.FL', 'cs.CC', connective: :or
q.and_not
q.author 'Tomáš Babiak'
end
end
expect(query.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=ti:%22Buchi+automata%22+AND+%28%28cat:%22cs.FL%22+OR+cat:%22cs.CC%22%29+ANDNOT+au:%22Tom%C3%A1%C5%A1+Babiak%22%29'
end
it do
query = subject.new.tap do |q|
q.title 'Buchi automata'
q.group do
q.author 'Tomáš Babiak'
q.or
q.category 'cs.FL', 'cs.CC', connective: :or
end
end
expect(query.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=ti:%22Buchi+automata%22+AND+%28au:%22Tom%C3%A1%C5%A1+Babiak%22+OR+%28cat:%22cs.FL%22+OR+cat:%22cs.CC%22%29%29'
end
it do
query = subject.new.tap do |q|
q.title 'Buchi automata'
q.group do
q.category 'cs.FL', 'cs.CC', connective: :and_not
end
end
expect(query.to_s).to eq 'sortBy=relevance&sortOrder=descending&search_query=ti:%22Buchi+automata%22+AND+%28%28cat:%22cs.FL%22+ANDNOT+cat:%22cs.CC%22%29%29'
end
end
end
context '#search_query?' do
subject { Query }

context 'with no search query' do
it { expect(subject.new.send :search_query?).to be false }
end
context 'with a search query' do
it { expect(subject.new.title('').send :search_query?).to be true }
end
end
context '#end_with_connective?' do
subject { Query }

context 'with no connective' do
it { expect(subject.new.send :end_with_connective?).to be false }
it { expect(subject.new.title('').send :end_with_connective?).to be false }
end
context 'with connective' do
Query::CONNECTIVES.keys.each do |connective|
context connective.to_s do
it { expect(subject.new.title('').send(connective).send :end_with_connective?).to be true }
end
end
end
end
context '#start_of_group?' do
subject { Query }

context 'with no start-of-group' do
it { expect(subject.new.send :start_of_group?).to be false }
it { expect(subject.new.title('a', 'b').send :start_of_group?).to be false }
end
context 'with a start-of-group' do
it do
query = subject.new.title('')
query.instance_variable_set "@query", query.to_s << "+#{CGI.escape '('}"
expect(query.send :start_of_group?).to be true
end
end
end
context '#parenthesize' do
subject { Query }

Expand Down

0 comments on commit d38cbeb

Please sign in to comment.