diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index 367c12a4e..93811b87e 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -132,8 +132,7 @@ module Searchable def where_array_count(attr, value) qualified_column = "cardinality(#{qualified_column_for(attr)})" - range = PostQueryBuilder.parse_range(value, :integer) - where_operator(qualified_column, *range) + where_numeric_matches(qualified_column, value) end # @param attr [String] the name of the JSON field @@ -172,8 +171,7 @@ module Searchable # value: "5", ">5", "<5", ">=5", "<=5", "5..10", "5,6,7" def where_numeric_matches(attribute, value, type = :integer) - range = PostQueryBuilder.parse_range(value, type) - where_operator(attribute, *range) + attribute_matches(value, attribute, type) end def where_boolean_matches(attribute, value) @@ -202,6 +200,25 @@ module Searchable end end + def attribute_matches(value, field, type = :integer) + operator, *args = PostQueryBuilder.parse_metatag_value(value, type) + relation = where_operator(field, operator, *args) + + # XXX Hack to make negating the equality operator work correctly on nullable columns. + # + # This makes `Post.attribute_matches(1, :approver_id)` produce `WHERE approver_id = 1 AND approver_id IS NOT NULL`. + # 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 + relation = relation.where.not(field => nil) + end + + relation + rescue PostQueryBuilder::ParseError + none + end + def search_attributes(params, attributes, current_user:) SearchContext.new(all, params, current_user).search_attributes(attributes) end diff --git a/app/models/post.rb b/app/models/post.rb index ac46f1531..7d40a8f6e 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1061,13 +1061,6 @@ class Post < ApplicationRecord end end - def attribute_matches(value, field, type = :integer) - operator, *args = PostQueryBuilder.parse_metatag_value(value, type) - where_operator(field, operator, *args) - rescue PostQueryBuilder::ParseError - none - end - def is_matches(value, current_user = User.anonymous) case value.downcase when "parent" @@ -1148,7 +1141,8 @@ class Post < ApplicationRecord when "pending", "flagged", "appealed", "modqueue", "deleted", "banned", "active", "unmoderated" where.not(parent: nil).where(parent: status_matches(parent)) when /\A\d+\z/ - where(id: parent).or(where(parent: parent)) + # XXX must use `attribute_matches(parent, :parent_id)` instead of `where(parent_id: parent)` so that `-parent:1` works + where(id: parent).or(attribute_matches(parent, :parent_id)) else none end @@ -1348,7 +1342,9 @@ class Post < ApplicationRecord else user = User.find_by_name(username) return none if user.nil? - where(approver: user) + + # XXX must use `attribute_matches(user.id, :approver_id)` instead of `where(approver: user)` so that `-approver:evazion` works + attribute_matches(user.id, :approver_id) end end diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index 1c9da0748..3e727fa63 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -331,30 +331,34 @@ class PostQueryBuilderTest < ActiveSupport::TestCase end should "return posts for the parent: metatag" do + post = create(:post) parent = create(:post) - child = create(:post, tag_string: "parent:#{parent.id}") + child = create(:post, parent: parent) - assert_tag_match([parent], "parent:none") + assert_tag_match([parent, post], "parent:none") assert_tag_match([child], "-parent:none") assert_tag_match([child], "parent:any") - assert_tag_match([parent], "-parent:any") + assert_tag_match([parent, post], "-parent:any") assert_tag_match([child, parent], "parent:#{parent.id}") assert_tag_match([child], "parent:#{child.id}") - assert_tag_match([], "-parent:#{parent.id}") - assert_tag_match([], "-parent:#{child.id}") + assert_tag_match([post], "-parent:#{parent.id}") + assert_tag_match([parent, post], "-parent:#{child.id}") assert_tag_match([child], "parent:#{parent.id} parent:#{child.id}") - assert_tag_match([child], "child:none") + assert_tag_match([], "parent:garbage") + assert_tag_match([child, parent, post], "-parent:garbage") + + assert_tag_match([child, post], "child:none") assert_tag_match([parent], "child:any") assert_tag_match([], "child:garbage") assert_tag_match([parent], "-child:none") - assert_tag_match([child], "-child:any") - assert_tag_match([child, parent], "-child:garbage") + assert_tag_match([child, post], "-child:any") + assert_tag_match([child, parent, post], "-child:garbage") end should "return posts when using the status of the parent/child" do @@ -442,7 +446,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase posts << create(:post, approver: nil) assert_tag_match([posts[0]], "approver:#{users[0].name}") - assert_tag_match([posts[1]], "-approver:#{users[0].name}") + assert_tag_match([posts[2], posts[1]], "-approver:#{users[0].name}") + assert_tag_match([posts[2], posts[0]], "-approver:#{users[1].name}") assert_tag_match([posts[1], posts[0]], "approver:any") assert_tag_match([posts[2]], "approver:none") assert_tag_match([posts[2]], "approver:NONE") @@ -1022,9 +1027,27 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([post], "pixiv_id:any") end - should "return posts for a pixiv_id:none search" do - post = create(:post) - assert_tag_match([post], "pixiv_id:none") + should "return posts for a pixiv_id: search" do + post1 = create(:post, pixiv_id: nil) + post2 = create(:post, pixiv_id: 42, source: "http://i1.pixiv.net/img-original/img/2014/10/02/13/51/23/42_p0.png") + + assert_tag_match([post2], "pixiv_id:42") + assert_tag_match([post1], "-pixiv_id:42") + + assert_tag_match([post2], "pixiv_id:>=42") + assert_tag_match([], "pixiv_id:<42") + + assert_tag_match([], "-pixiv_id:>=42") + assert_tag_match([post2], "-pixiv_id:<42") + + assert_tag_match([post1], "pixiv_id:none") + assert_tag_match([post2], "pixiv_id:any") + + assert_tag_match([post2], "-pixiv_id:none") + assert_tag_match([post1], "-pixiv_id:any") + + assert_tag_match([post1], "pixiv:none") + assert_tag_match([post2], "pixiv:any") end should "return posts for the search: metatag" do