Skip to content

Commit

Permalink
Add not feature with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vasilakisfil committed Feb 12, 2017
1 parent 9a333df commit 8dc46b5
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 22 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,19 @@ You can apply an `OR` on associations as well or even nested ones, there isn't m
I suggest you though to take a look on the [tests](spec/tests/or_filter_spec.rb), cause the syntax gets a bit complex after a while ;)


### NOT Filter
You can apply an SQL `NOT` (for ActiveRecord 4+) using the following syntax:
`{not: {name: 'Filippos', email: {ends_with: '@gmail.com'}}}`

It will generate: `WHERE (NOT (users.name = 'Filippos')) AND (NOT (users.email LIKE '%@gmail.com'))`

You can apply an `NOT` on associations as well or even nested ones, there isn't much limitation on that.
I suggest you to also take a look on the [tests](spec/tests/not_filter_spec.rb).

Also I should note that you need to add specific (partial) queries if you don't want
to have performance issues on tables with millions of rows.


### Scopes
**Filtering on scopes is not enabled by default**.

Expand Down
114 changes: 95 additions & 19 deletions lib/active_hash_relation/column_filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ module ActiveHashRelation::ColumnFilters
def filter_integer(resource, column, table_name, param)
if param.is_a? Array
n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
return resource.where("#{table_name}.#{column} IN (#{n_param})")
if @is_not
return resource.where.not("#{table_name}.#{column} IN (#{n_param})")
else
return resource.where("#{table_name}.#{column} IN (#{n_param})")
end
elsif param.is_a? Hash
if !param[:null].nil?
return null_filters(resource, table_name, column, param)
else
return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
end
else
return resource.where("#{table_name}.#{column} = ?", param)
if @is_not
return resource.where.not("#{table_name}.#{column} = ?", param)
else
return resource.where("#{table_name}.#{column} = ?", param)
end
end
end

Expand All @@ -25,15 +33,23 @@ def filter_decimal(resource, column, table_name, param)
def filter_string(resource, column, table_name, param)
if param.is_a? Array
n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
return resource.where("#{table_name}.#{column} IN (#{n_param})")
if @is_not
return resource.where.not("#{table_name}.#{column} IN (#{n_param})")
else
return resource.where("#{table_name}.#{column} IN (#{n_param})")
end
elsif param.is_a? Hash
if !param[:null].nil?
return null_filters(resource, table_name, column, param)
else
return apply_like_filters(resource, table_name, column, param)
end
else
return resource.where("#{table_name}.#{column} = ?", param)
if @is_not
return resource.where.not("#{table_name}.#{column} = ?", param)
else
return resource.where("#{table_name}.#{column} = ?", param)
end
end
end

Expand All @@ -44,15 +60,23 @@ def filter_text(resource, column, param)
def filter_date(resource, column, table_name, param)
if param.is_a? Array
n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
return resource.where("#{table_name}.#{column} IN (#{n_param})")
if @is_not
return resource.where.not("#{table_name}.#{column} IN (#{n_param})")
else
return resource.where("#{table_name}.#{column} IN (#{n_param})")
end
elsif param.is_a? Hash
if !param[:null].nil?
return null_filters(resource, table_name, column, param)
else
return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
end
else
resource = resource.where(column => param)
if @is_not
resource = resource.where.not(column => param)
else
resource = resource.where(column => param)
end
end

return resource
Expand All @@ -61,15 +85,23 @@ def filter_date(resource, column, table_name, param)
def filter_datetime(resource, column, table_name, param)
if param.is_a? Array
n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
return resource = resource.where("#{table_name}.#{column} IN (#{n_param})")
if @is_not
return resource = resource.where.not("#{table_name}.#{column} IN (#{n_param})")
else
return resource = resource.where("#{table_name}.#{column} IN (#{n_param})")
end
elsif param.is_a? Hash
if !param[:null].nil?
return null_filters(resource, table_name, column, param)
else
return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
end
else
resource = resource.where(column => param)
if @is_not
resource = resource.where.not(column => param)
else
resource = resource.where(column => param)
end
end

return resource
Expand All @@ -85,7 +117,11 @@ def filter_boolean(resource, column, table_name, param)
b_param = ActiveRecord::Type::Boolean.new.type_cast_from_database(param)
end

resource = resource.where(column => b_param)
if @is_not
resource = resource.where.not(column => b_param)
else
resource = resource.where(column => b_param)
end
end
end

Expand All @@ -95,15 +131,31 @@ def apply_leq_geq_le_ge_filters(resource, table_name, column, param)
return resource.where("#{table_name}.#{column} = ?", param[:eq]) if param[:eq]

if !param[:leq].blank?
resource = resource.where("#{table_name}.#{column} <= ?", param[:leq])
if @is_not
resource = resource.where.not("#{table_name}.#{column} <= ?", param[:leq])
else
resource = resource.where("#{table_name}.#{column} <= ?", param[:leq])
end
elsif !param[:le].blank?
resource = resource.where("#{table_name}.#{column} < ?", param[:le])
if @is_not
resource = resource.where.not("#{table_name}.#{column} < ?", param[:le])
else
resource = resource.where("#{table_name}.#{column} < ?", param[:le])
end
end

if !param[:geq].blank?
resource = resource.where("#{table_name}.#{column} >= ?", param[:geq])
if @is_not
resource = resource.where.not("#{table_name}.#{column} >= ?", param[:geq])
else
resource = resource.where("#{table_name}.#{column} >= ?", param[:geq])
end
elsif !param[:ge].blank?
resource = resource.where("#{table_name}.#{column} > ?", param[:ge])
if @is_not
resource = resource.where.not("#{table_name}.#{column} > ?", param[:ge])
else
resource = resource.where("#{table_name}.#{column} > ?", param[:ge])
end
end

return resource
Expand All @@ -114,31 +166,55 @@ def apply_like_filters(resource, table_name, column, param)
like_method = "ILIKE" if param[:with_ilike]

if !param[:starts_with].blank?
resource = resource.where("#{table_name}.#{column} #{like_method} ?", "#{param[:starts_with]}%")
if @is_not
resource = resource.where.not("#{table_name}.#{column} #{like_method} ?", "#{param[:starts_with]}%")
else
resource = resource.where("#{table_name}.#{column} #{like_method} ?", "#{param[:starts_with]}%")
end
end

if !param[:ends_with].blank?
resource = resource.where("#{table_name}.#{column} #{like_method} ?", "%#{param[:ends_with]}")
if @is_not
resource = resource.where.not("#{table_name}.#{column} #{like_method} ?", "%#{param[:ends_with]}")
else
resource = resource.where("#{table_name}.#{column} #{like_method} ?", "%#{param[:ends_with]}")
end
end

if !param[:like].blank?
resource = resource.where("#{table_name}.#{column} #{like_method} ?", "%#{param[:like]}%")
if @is_not
resource = resource.where.not("#{table_name}.#{column} #{like_method} ?", "%#{param[:like]}%")
else
resource = resource.where("#{table_name}.#{column} #{like_method} ?", "%#{param[:like]}%")
end
end

if !param[:eq].blank?
resource = resource.where("#{table_name}.#{column} = ?", param[:eq])
if @is_not
resource = resource.where.not("#{table_name}.#{column} = ?", param[:eq])
else
resource = resource.where("#{table_name}.#{column} = ?", param[:eq])
end
end

return resource
end

def null_filters(resource, table_name, column, param)
if param[:null] == true
resource = resource.where("#{table_name}.#{column} IS NULL")
if @is_not
resource = resource.where.not("#{table_name}.#{column} IS NULL")
else
resource = resource.where("#{table_name}.#{column} IS NULL")
end
end

if param[:null] == false
resource = resource.where("#{table_name}.#{column} IS NOT NULL")
if @is_not
resource = resource.where.not("#{table_name}.#{column} IS NOT NULL")
else
resource = resource.where("#{table_name}.#{column} IS NOT NULL")
end
end

return resource
Expand Down
17 changes: 15 additions & 2 deletions lib/active_hash_relation/filter_applier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class FilterApplier

attr_reader :configuration

def initialize(resource, params, include_associations: false, model: nil)
def initialize(resource, params, include_associations: false, model: nil, is_not: false)
@configuration = Module.nesting.last.configuration
@resource = resource
if params.respond_to?(:to_unsafe_h)
Expand All @@ -19,10 +19,12 @@ def initialize(resource, params, include_associations: false, model: nil)
end
@include_associations = include_associations
@model = find_model(model)
is_not ? @is_not = true : @is_not = false
end

def apply_filters
run_or_filters
run_not_filters

table_name = @model.table_name
@model.columns.each do |c|
Expand Down Expand Up @@ -68,7 +70,7 @@ def filter_class(resource_name)
def run_or_filters
if @params[:or].is_a?(Array)
if ActiveRecord::VERSION::MAJOR < 5
return Rails.logger.warn("OR query is supported on ActiveRecord 5+")
return Rails.logger.warn("OR query is supported on ActiveRecord 5+")
end

if @params[:or].length >= 2
Expand All @@ -83,5 +85,16 @@ def run_or_filters
end
end
end

def run_not_filters
if @params[:not].is_a?(Hash) && !@params[:not].blank?
@resource = self.class.new(
@resource,
@params[:not],
include_associations: @include_associations,
is_not: true
).apply_filters
end
end
end
end
2 changes: 1 addition & 1 deletion lib/active_hash_relation/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ActiveHashRelation
VERSION = "1.2.0"
VERSION = "1.3.0"
end
105 changes: 105 additions & 0 deletions spec/tests/not_filter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
describe ActiveHashRelation do
include Helpers

context 'NOT filter' do
it "one NOT clause" do
hash = {not: {name: 'Filippos'}}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users WHERE (NOT (users.name = 'Filippos'))"
)

expect(strip(query)).to eq expected_query.to_s
end

it "multiple NOT clauses" do
hash = {not: {name: 'Filippos', email: '[email protected]'}}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users WHERE",
"(NOT (users.name = 'Filippos'))",
"AND",
"(NOT (users.email = '[email protected]'))"
)

expect(strip(query)).to eq expected_query.to_s
end

if ActiveRecord::VERSION::MAJOR >= 5
it "NOT clause inside OR clause" do
hash = {or: [{not: {name: 'Filippos', token: '123'}}, {not: {name: 'Vasilis'}}]}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users",
"WHERE",
"(",
"(NOT (users.name = 'Filippos')) AND (NOT (users.token = '123'))",
"OR",
"(NOT (users.name = 'Vasilis'))",
")"
)

expect(strip(query)).to eq expected_query.to_s
end
end

it "complex NOT clause" do
hash = {not: {name: 'Filippos', email: {ends_with: '@gmail.com'}}}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users",
"WHERE",
"(NOT (users.name = 'Filippos'))",
"AND",
"(NOT (users.email LIKE '%@gmail.com'))",
)

expect(strip(query)).to eq expected_query.to_s
end

context "on NULL" do
it "NOT clause on null" do
hash = {not: {name: {null: true}}}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users WHERE (NOT (users.name IS NULL))"
)

expect(strip(query)).to eq expected_query.to_s
end

it "NOT clause on not null" do
hash = {not: {name: {null: false}}}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users WHERE (NOT (users.name IS NOT NULL))"
)

expect(strip(query)).to eq expected_query.to_s
end
end

it "NOT clause on associations" do
hash = {microposts: {not: {content: 'Sveavägen 4', id: 1}}}

query = HelperClass.new.apply_filters(User.all, hash, include_associations: true).to_sql
expected_query = q(
"SELECT users.* FROM users",
"INNER JOIN microposts ON microposts.user_id = users.id",
"WHERE",
"(NOT (microposts.id = 1))",
"AND",
"(NOT (microposts.content = 'Sveavägen 4'))"
)

expect(strip(query)).to eq expected_query.to_s
end

end
end

0 comments on commit 8dc46b5

Please sign in to comment.