Skip to content

Commit

Permalink
Have the subset_conditions plugin support where_all and where_any met…
Browse files Browse the repository at this point in the history
…hods to combine existing subsets with AND or OR

This DRYs up code such as:

  plugin :subset_conditions

  dataset_module do
    where :a, :a
    where :b, :b

    where :a_and_b do
      where(a_conditions).where(b_conditions)
    end

    where :a_or_b do
      where(a_conditions).or(b_conditions)
    end
  end

Now you can do:

  plugin :subset_conditions

  dataset_module do
    where :a, :a
    where :b, :b
    where_all :a_and_b, :a, :b
    where_any :a_or_b, :a, :b
  end

In addition to being DRYer, this also makes the a_and_b and a_or_b
methods faster, as they will support caching in the same way that
the a and b methods do.

Additionally, make the subset_conditions plugin create *_conditions
methods for subsets created by the exclude method.  This makes it
so you don't have to manually invert the conditions and use the
where or subset method in order to get the *_conditions method
created, and allows filters created with exclude to be used as
input to the where_all and where_any methods.
  • Loading branch information
jeremyevans committed Oct 28, 2024
1 parent e566408 commit fe99976
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
=== master

* Have the subset_conditions plugin support where_all and where_any methods to combine existing subsets with AND or OR (jeremyevans)

* Have the subset_conditions plugin add *_conditions methods for exclude method calls in addition to where and subset (jeremyevans)

* Use Ruby module naming instead of Java package naming for access to Java libraries in the jdbc adapters (kalenp) (#2235, #2236)

* Make schema_dumper extension format options similar to Hash#inspect on Ruby 3.4+ (jeremyevans)
Expand Down
89 changes: 84 additions & 5 deletions lib/sequel/plugins/subset_conditions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@
module Sequel
module Plugins
# The subset_conditions plugin creates an additional *_conditions method
# for every subset created, which returns the filter conditions the subset
# uses. This can be useful if you want to use the conditions for a separate
# filter or combine them with OR.
# for every `subset`, `where`, and `exclude` method call in a dataset_module
# block. This method returns the filter conditions, which can be useful if
# you want to use the conditions for a separate filter or combine them with OR.
# It also supports where_all and where_any dataset_module methods for
# combining multiple dataset method filters with AND or OR.
#
# Usage:
#
# # Add subset_conditions in the Album class
# Album.plugin :subset_conditions
#
# # This will now create a published_conditions method
# Album.dataset_module do
# subset :published, published: true
# # This will now create a published_conditions method
# where :published, published: true
#
# # This will now create a not_bad_conditions method
# exclude :not_bad, :bad
#
# # This will create good_and_available and
# # good_and_available_conditions methods
# where_all :good_and_available, :published, :not_bad
#
# # This will create good_or_available and
# # good_or_available_conditions methods
# where_any :good_or_available, :published, :not_bad
# end
#
# Album.where(Album.published_conditions).sql
Expand All @@ -25,6 +38,12 @@ module Plugins
#
# Album.where(Album.published_conditions | {ready: true}).sql
# # SELECT * FROM albums WHERE ((published IS TRUE) OR (ready IS TRUE))
#
# Album.good_and_available.sql
# SELECT * FROM albums WHERE ((published IS TRUE) AND NOT bad)
#
# Album.good_or_available.sql
# SELECT * FROM albums WHERE ((published IS TRUE) OR NOT bad)
module SubsetConditions
def self.apply(model, &block)
model.instance_exec do
Expand All @@ -42,6 +61,66 @@ def where(name, *args, &block)
cond = cond.first if cond.size == 1
define_method(:"#{name}_conditions"){filter_expr(cond, &block)}
end

# Also create a method that returns the conditions the filter uses.
def exclude(name, *args, &block)
super
cond = args
cond = cond.first if cond.size == 1
define_method(:"#{name}_conditions"){Sequel.~(filter_expr(cond, &block))}
end

# Create a method that combines filters from already registered
# dataset methods, and filters for rows where all of the conditions
# are satisfied.
#
# Employee.dataset_module do
# where :active, active: true
# where :started, Sequel::CURRENT_DATE <= :start_date
# where_all(:active_and_started, :active, :started)
# end
#
# Employee.active_and_started.sql
# # SELECT * FROM employees WHERE ((active IS TRUE) AND (CURRENT_DATE <= start_date))
def where_all(name, *args)
_where_any_all(:&, name, args)
end

# Create a method that combines filters from already registered
# dataset methods, and filters for rows where any of the conditions
# are satisfied.
#
# Employee.dataset_module do
# where :active, active: true
# where :started, Sequel::CURRENT_DATE <= :start_date
# where_any(:active_or_started, :active, :started)
# end
#
# Employee.active_or_started.sql
# # SELECT * FROM employees WHERE ((active IS TRUE) OR (CURRENT_DATE <= start_date))
def where_any(name, *args)
_where_any_all(:|, name, args)
end

private

if RUBY_VERSION >= '2'
# Backbone of #where_any and #where_all
def _where_any_all(meth, name, args)
ds = model.dataset
# #bind used here because the dataset module may not yet be included in the model's dataset
where(name, Sequel.send(meth, *args.map{|a| self.instance_method(:"#{a}_conditions").bind(ds).call}))
end
else
# Cannot bind module method to arbitrary objects in Ruby 1.9.
# :nocov:
def _where_any_all(meth, name, args)
ds = model.dataset.clone
ds.extend(self)
where(name, Sequel.send(meth, *args.map{|a| ds.send(:"#{a}_conditions")}))
end
# :nocov:
end
end
end
end
Expand Down
29 changes: 29 additions & 0 deletions spec/extensions/subset_conditions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
@c.dataset_module{where(:active, :active)}
@c.where(@c.active_conditions).sql.must_equal @c.active.sql

@c.dataset_module{exclude(:not_bad, :bad)}
@c.where(@c.not_bad_conditions).sql.must_equal @c.not_bad.sql

@c.dataset_module{subset(:active_published, Sequel.&(:active, :published => true))}
@c.where(@c.active_published_conditions).sql.must_equal @c.active_published.sql
@c.where(Sequel.&(@c.active_conditions, @c.published_conditions)).sql.must_equal @c.active_published.sql
@c.where(Sequel.|(@c.active_conditions, @c.published_conditions)).sql.must_equal "SELECT * FROM a WHERE (active OR (published IS TRUE))"
@c.where(Sequel.|(@c.active_published_conditions, :foo)).sql.must_equal "SELECT * FROM a WHERE ((active AND (published IS TRUE)) OR foo)"

@c.dataset_module{exclude(:not_x_or_y, :x){:y}}
@c.where(@c.not_x_or_y_conditions).sql.must_equal @c.not_x_or_y.sql

end

it "should work with blocks" do
Expand All @@ -35,4 +42,26 @@
@c.where(Sequel.|(@c.active_conditions, @c.published_conditions)).sql.must_equal "SELECT * FROM a WHERE (active OR (published IS TRUE))"
@c.where(Sequel.|(@c.active_published_conditions, :foo)).sql.must_equal "SELECT * FROM a WHERE ((active AND (published IS TRUE)) OR foo)"
end

it "should support where_all and where_any for combining subset conditions" do
@c.dataset_module do
subset(:published, :published => true)
where(:active, :active)
exclude(:not_bad, :bad)

where_all(:active_all1, :active)
where_any(:active_any1, :active)
where_all(:active_and_published, :active, :published)
where_any(:active_or_published, :active, :published)
where_all(:active_and_published_and_not_bad, :active, :published, :not_bad)
where_any(:active_or_published_or_not_bad, :active, :published, :not_bad)
end

@c.active_all1.sql.must_equal 'SELECT * FROM a WHERE active'
@c.active_any1.sql.must_equal 'SELECT * FROM a WHERE active'
@c.active_and_published.sql.must_equal 'SELECT * FROM a WHERE (active AND (published IS TRUE))'
@c.active_or_published.sql.must_equal 'SELECT * FROM a WHERE (active OR (published IS TRUE))'
@c.active_and_published_and_not_bad.sql.must_equal 'SELECT * FROM a WHERE (active AND (published IS TRUE) AND NOT bad)'
@c.active_or_published_or_not_bad.sql.must_equal 'SELECT * FROM a WHERE (active OR (published IS TRUE) OR NOT bad)'
end
end

0 comments on commit fe99976

Please sign in to comment.