From 331f15259a6a811671e814cda251a3da4da35ca3 Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 27 Sep 2022 01:26:01 -0500 Subject: [PATCH] Fix #1590: id metatag: "id:A..B,C..D,E" Support searches like the following: * score:<0,>100 (equivalent to `score:<0 or score:>100`) * score:5,10..15,>20 (equivalent to `score:5 or score:10..15 or score:>20`) * id:5,10..15,20..25,30 (equivalent to `id:5 or id:10..15 or id:20..25 or id:30`) This also works inside the `search[id]` URL parameter, and inside other numeric URL search parameters. --- app/logical/concerns/searchable.rb | 14 +++++++++++--- app/logical/range_parser.rb | 7 +++++-- test/unit/post_query_builder_test.rb | 29 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index 10a963d7e..f5c3110aa 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -201,8 +201,16 @@ module Searchable end def attribute_matches(value, field, type = :integer) - operator, *args = RangeParser.parse(value, type) - relation = where_operator(field, operator, *args) + operator, arg = RangeParser.parse(value, type) + + if operator == :union + # operator = :union, arg = [[:eq, 5], [:gt, 7], [:lt, 3]] + relation = arg.map do |sub_operator, sub_value| + where_operator(field, sub_operator, sub_value) + end.reduce(:or) + else + relation = where_operator(field, operator, arg) + end # XXX Hack to make negating the equality operator work correctly on nullable columns. # @@ -210,7 +218,7 @@ module Searchable # This way if the relation is negated with `Post.attribute_matches(1, :approver_id).negate_relation`, it will # produce `WHERE approver_id != 1 OR approver_id IS NULL`. This is so the search includes NULL values; if it # was just `approver_id != 1`, then it would not include when approver_id is NULL. - if (operator in :eq | :not_eq) && args[0] != nil && has_attribute?(field) && column_for_attribute(field).null + if (operator in :eq | :not_eq) && arg != nil && has_attribute?(field) && column_for_attribute(field).null relation = relation.where.not(field => nil) end diff --git a/app/logical/range_parser.rb b/app/logical/range_parser.rb index 617727d07..16e6f6b3d 100644 --- a/app/logical/range_parser.rb +++ b/app/logical/range_parser.rb @@ -15,6 +15,7 @@ # RangeParser.parse("5..10") => [:between, (5..10)] # RangeParser.parse("5...10") => [:between, (5...10)] # RangeParser.parse("5,6,7") => [:in, [5, 6, 7]] +# RangeParser.parse("5,7..9") => [:union, [[:eq, 5], [:between, (7..9)]]] # RangeParser.parse("any") => [:not_eq, nil] # RangeParser.parse("none") => [:eq, nil] # @@ -40,6 +41,10 @@ class RangeParser range = case string in _ if type == :enum [:in, string.split(/[, ]+/).map { |x| parse_value(x) }] + in /[, ]/ if string.match?(/<|>|\.\./) # >A,(.+)/ # >A [:gt, parse_value($1)] - in /[, ]/ # A,B,C - [:in, string.split(/[, ]+/).map { |x| parse_value(x) }] in "any" [:not_eq, nil] in "none" diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index 3e727fa63..c65b6ff56 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -212,6 +212,19 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[1], posts[0]], "id:#{posts[1].id}..#{posts[0].id}") assert_tag_match([posts[1], posts[0]], "id:#{posts[0].id}...#{posts[2].id}") + assert_tag_match(posts.reverse, "id:#{posts[0].id},#{posts[1].id}..#{posts[2].id}") + assert_tag_match(posts.reverse, "id:#{posts[0].id}..#{posts[1].id},#{posts[2].id}") + assert_tag_match(posts.reverse, "id:#{posts[0].id},>=#{posts[1].id}") + assert_tag_match(posts.reverse, "id:<=#{posts[1].id},#{posts[2].id}") + + assert_tag_match([], "id:<#{posts[0].id},>#{posts[2].id}") + assert_tag_match([posts[2], posts[0]], "id:<=#{posts[0].id},>=#{posts[2].id}") + assert_tag_match([posts[2], posts[0]], "id:..#{posts[0].id},#{posts[2].id}..") + + assert_tag_match([posts[1]], "id:<#{posts[0].id},#{posts[1].id},>#{posts[2].id}") + assert_tag_match([posts[1]], "id:#{posts[1].id},<#{posts[0].id},>#{posts[2].id}") + assert_tag_match([posts[1]], "id:<#{posts[0].id},>#{posts[2].id},#{posts[1].id}") + assert_tag_match([posts[1], posts[0]], "-id:>#{posts[1].id}") assert_tag_match([posts[2], posts[1]], "-id:<#{posts[1].id}") assert_tag_match([posts[0]], "-id:>=#{posts[1].id}") @@ -219,6 +232,19 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[0]], "-id:#{posts[1].id}..#{posts[2].id}") assert_tag_match([posts[0]], "-id:#{posts[1].id},#{posts[2].id}") + assert_tag_match([], "-id:#{posts[0].id},#{posts[1].id}..#{posts[2].id}") + assert_tag_match([], "-id:#{posts[0].id}..#{posts[1].id},#{posts[2].id}") + assert_tag_match([], "-id:#{posts[0].id},>=#{posts[1].id}") + assert_tag_match([], "-id:<=#{posts[1].id},#{posts[2].id}") + + assert_tag_match(posts.reverse, "-id:<#{posts[0].id},>#{posts[2].id}") + assert_tag_match([posts[1]], "-id:<=#{posts[0].id},>=#{posts[2].id}") + assert_tag_match([posts[1]], "-id:..#{posts[0].id},#{posts[2].id}..") + + assert_tag_match([posts[2], posts[0]], "-id:<#{posts[0].id},#{posts[1].id},>#{posts[2].id}") + assert_tag_match([posts[2], posts[0]], "-id:#{posts[1].id},<#{posts[0].id},>#{posts[2].id}") + assert_tag_match([posts[2], posts[0]], "-id:<#{posts[0].id},>#{posts[2].id},#{posts[1].id}") + assert_tag_match([], "id:#{posts[0].id} id:#{posts[2].id}") assert_tag_match([posts[0]], "-id:#{posts[1].id} -id:#{posts[2].id}") assert_tag_match([posts[1]], "id:>#{posts[0].id} id:<#{posts[2].id}") @@ -1048,6 +1074,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([post1], "pixiv:none") assert_tag_match([post2], "pixiv:any") + + assert_tag_match([], "-pixiv_id:>40,<50") + assert_tag_match([post2], "-pixiv_id:<40,>50") end should "return posts for the search: metatag" do