From fb4c8d4efe4c1933fb2f61b1e23b7dfdfdd41016 Mon Sep 17 00:00:00 2001 From: ksadoff Date: Mon, 23 Nov 2020 10:54:38 -0500 Subject: [PATCH] MONGOID-4906 Implement bitwise operators (#4917) --- lib/mongoid/matcher.rb | 5 + lib/mongoid/matcher/bits.rb | 41 +++++ lib/mongoid/matcher/bits_all_clear.rb | 20 +++ lib/mongoid/matcher/bits_all_set.rb | 20 +++ lib/mongoid/matcher/bits_any_clear.rb | 20 +++ lib/mongoid/matcher/bits_any_set.rb | 20 +++ lib/mongoid/matcher/field_operator.rb | 4 + .../matcher_operator_data/bits_all_clear.yml | 159 ++++++++++++++++++ .../matcher_operator_data/bits_all_set.yml | 159 ++++++++++++++++++ .../matcher_operator_data/bits_any_clear.yml | 159 ++++++++++++++++++ .../matcher_operator_data/bits_any_set.yml | 159 ++++++++++++++++++ 11 files changed, 766 insertions(+) create mode 100644 lib/mongoid/matcher/bits.rb create mode 100644 lib/mongoid/matcher/bits_all_clear.rb create mode 100644 lib/mongoid/matcher/bits_all_set.rb create mode 100644 lib/mongoid/matcher/bits_any_clear.rb create mode 100644 lib/mongoid/matcher/bits_any_set.rb create mode 100644 spec/integration/matcher_operator_data/bits_all_clear.yml create mode 100644 spec/integration/matcher_operator_data/bits_all_set.yml create mode 100644 spec/integration/matcher_operator_data/bits_any_clear.yml create mode 100644 spec/integration/matcher_operator_data/bits_any_set.yml diff --git a/lib/mongoid/matcher.rb b/lib/mongoid/matcher.rb index 7a1b01b94b..08680f5f64 100644 --- a/lib/mongoid/matcher.rb +++ b/lib/mongoid/matcher.rb @@ -103,6 +103,11 @@ module Matcher require 'mongoid/matcher/all' require 'mongoid/matcher/and' +require 'mongoid/matcher/bits' +require 'mongoid/matcher/bits_all_clear' +require 'mongoid/matcher/bits_all_set' +require 'mongoid/matcher/bits_any_clear' +require 'mongoid/matcher/bits_any_set' require 'mongoid/matcher/elem_match' require 'mongoid/matcher/elem_match_expression' require 'mongoid/matcher/eq' diff --git a/lib/mongoid/matcher/bits.rb b/lib/mongoid/matcher/bits.rb new file mode 100644 index 0000000000..7c5996861c --- /dev/null +++ b/lib/mongoid/matcher/bits.rb @@ -0,0 +1,41 @@ +module Mongoid + module Matcher + + # @api private + module Bits + def matches?(exists, value, condition) + case value + when BSON::Binary + value = value.data.split('').map { |n| '%02x' % n.ord }.join.to_i(16) + end + case condition + when Array + array_matches?(value, condition) + when BSON::Binary + int_cond = condition.data.split('').map { |n| '%02x' % n.ord }.join.to_i(16) + int_matches?(value, int_cond) + when Integer + if condition < 0 + raise Errors::InvalidQuery, "Invalid value for $#{operator_name} argument: negative integers are not allowed: #{condition}" + end + int_matches?(value, condition) + when Float + if (int_cond = condition.to_i).to_f == condition + if int_cond < 0 + raise Errors::InvalidQuery, "Invalid value for $#{operator_name} argument: negative numbers are not allowed: #{condition}" + end + int_matches?(value, int_cond) + else + raise Errors::InvalidQuery, "Invalid type for $#{operator_name} argument: not representable as an integer: #{condition}" + end + else + raise Errors::InvalidQuery, "Invalid type for $#{operator_name} argument: #{condition}" + end + end + + module_function def operator_name + name.sub(/.*::/, '').sub(/\A(.)/) { |l| l.downcase } + end + end + end +end diff --git a/lib/mongoid/matcher/bits_all_clear.rb b/lib/mongoid/matcher/bits_all_clear.rb new file mode 100644 index 0000000000..6aa274a149 --- /dev/null +++ b/lib/mongoid/matcher/bits_all_clear.rb @@ -0,0 +1,20 @@ +module Mongoid + module Matcher + + # @api private + module BitsAllClear + include Bits + extend self + + def array_matches?(value, condition) + condition.all? do |c| + value & (1< 0 + end + end + + def int_matches?(value, condition) + value & condition == condition + end + end + end +end diff --git a/lib/mongoid/matcher/bits_any_clear.rb b/lib/mongoid/matcher/bits_any_clear.rb new file mode 100644 index 0000000000..1733d4de6b --- /dev/null +++ b/lib/mongoid/matcher/bits_any_clear.rb @@ -0,0 +1,20 @@ +module Mongoid + module Matcher + + # @api private + module BitsAnyClear + include Bits + extend self + + def array_matches?(value, condition) + condition.any? do |c| + value & (1< 0 + end + end + + def int_matches?(value, condition) + value & condition > 0 + end + end + end +end diff --git a/lib/mongoid/matcher/field_operator.rb b/lib/mongoid/matcher/field_operator.rb index 37f4a6a2e9..99fb656307 100644 --- a/lib/mongoid/matcher/field_operator.rb +++ b/lib/mongoid/matcher/field_operator.rb @@ -5,6 +5,10 @@ module Matcher module FieldOperator MAP = { '$all' => All, + '$bitsAllClear' => BitsAllClear, + '$bitsAllSet' => BitsAllSet, + '$bitsAnyClear' => BitsAnyClear, + '$bitsAnySet' => BitsAnySet, '$elemMatch' => ElemMatch, '$eq' => Eq, '$exists' => Exists, diff --git a/spec/integration/matcher_operator_data/bits_all_clear.yml b/spec/integration/matcher_operator_data/bits_all_clear.yml new file mode 100644 index 0000000000..efaa3d5dff --- /dev/null +++ b/spec/integration/matcher_operator_data/bits_all_clear.yml @@ -0,0 +1,159 @@ +- name: existing field - matches with int + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: 35 + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with int + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: 24 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with binData + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with binData + document: + a: 21 + binaryValueofA: "00010101" + query: + a: + $bitsAllClear: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + matches: false + min_server_version: 3.2 + +- name: existing binData field matches + document: + a: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + query: + a: + $bitsAllClear: 20 + matches: true + min_server_version: 3.2 + +- name: existing binData field does not match + document: + a: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + query: + a: + $bitsAllClear: 15 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: [0, 3] + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: [0, 2] + matches: false + min_server_version: 3.2 + +- name: float condition representable as an integer + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: 35.0 + matches: true + min_server_version: 3.2 + +- name: float condition not representable as an integer + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: 35.1 + error: true + min_server_version: 3.2 + +- name: string condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllClear: hello + error: true + min_server_version: 3.2 + +- name: empty array condition + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: [] + matches: true + min_server_version: 3.2 + +- name: integer 0 condition + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllClear: 0 + matches: true + min_server_version: 3.2 + +- name: negative integer condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllClear: -1 + error: true + min_server_version: 3.2 + +- name: negative float condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllClear: -1.0 + error: true + min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/bits_all_set.yml b/spec/integration/matcher_operator_data/bits_all_set.yml new file mode 100644 index 0000000000..87d60305a6 --- /dev/null +++ b/spec/integration/matcher_operator_data/bits_all_set.yml @@ -0,0 +1,159 @@ +- name: existing field - matches with int + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: 50 + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with int + document: + a: 52 + binaryValueofA: "00110100" + query: + a: + $bitsAllSet: 50 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with binData + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: !ruby/object:BSON::Binary + data: !binary |- + Ng== + type: :generic + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with binData + document: + a: 21 + binaryValueofA: "00010101" + query: + a: + $bitsAllSet: !ruby/object:BSON::Binary + data: !binary |- + MC== + type: :generic + matches: false + min_server_version: 3.2 + +- name: existing binData field matches + document: + a: !ruby/object:BSON::Binary + data: !binary |- + Ng== + type: :generic + query: + a: + $bitsAllSet: 48 + matches: true + min_server_version: 3.2 + +- name: existing binData field does not match + document: + a: !ruby/object:BSON::Binary + data: !binary |- + MC== + type: :generic + query: + a: + $bitsAllSet: 54 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllSet: [2, 4] + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAllSet: [0, 3] + matches: false + min_server_version: 3.2 + +- name: float condition representable as an integer + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: 50.0 + matches: true + min_server_version: 3.2 + +- name: float condition not representable as an integer + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: 50.1 + error: true + min_server_version: 3.2 + +- name: string condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: hello + error: true + min_server_version: 3.2 + +- name: empty array condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: [] + matches: true + min_server_version: 3.2 + +- name: integer 0 condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: 0 + matches: true + min_server_version: 3.2 + +- name: negative integer condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: -1 + error: true + min_server_version: 3.2 + +- name: negative float condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAllSet: -1.0 + error: true + min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/bits_any_clear.yml b/spec/integration/matcher_operator_data/bits_any_clear.yml new file mode 100644 index 0000000000..e3b392fdb5 --- /dev/null +++ b/spec/integration/matcher_operator_data/bits_any_clear.yml @@ -0,0 +1,159 @@ +- name: existing field - matches with int + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: 35 + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with int + document: + a: 7 + binaryValueofA: "00000011" + query: + a: + $bitsAnyClear: 35 + matches: true + min_server_version: 3.2 + +- name: existing field - matches with binData + document: + a: 16 + binaryValueofA: "00001000" + query: + a: + $bitsAnyClear: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with binData + document: + a: 55 + binaryValueofA: "00110111" + query: + a: + $bitsAnyClear: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + matches: false + min_server_version: 3.2 + +- name: existing binData field matches + document: + a: !ruby/object:BSON::Binary + data: !binary |- + FA== + type: :generic + query: + a: + $bitsAnyClear: 35 + matches: true + min_server_version: 3.2 + +- name: existing binData field does not match + document: + a: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + query: + a: + $bitsAnyClear: 32 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAnyClear: [0, 2] + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAnyClear: [2, 4] + matches: false + min_server_version: 3.2 + +- name: float condition representable as an integer + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: 35.0 + matches: true + min_server_version: 3.2 + +- name: float condition not representable as an integer + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: 35.1 + error: true + min_server_version: 3.2 + +- name: string condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: 'hello' + error: true + min_server_version: 3.2 + +- name: empty array condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: [] + matches: false + min_server_version: 3.2 + +- name: integer 0 condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: 0 + matches: false + min_server_version: 3.2 + +- name: negative integer condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: -1 + error: true + min_server_version: 3.2 + +- name: negative float condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnyClear: -1.0 + error: true + min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/bits_any_set.yml b/spec/integration/matcher_operator_data/bits_any_set.yml new file mode 100644 index 0000000000..f95d328167 --- /dev/null +++ b/spec/integration/matcher_operator_data/bits_any_set.yml @@ -0,0 +1,159 @@ +- name: existing field - matches with int + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: 35 + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with int + document: + a: 16 + binaryValueofA: "00000100" + query: + a: + $bitsAllSet: 35 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with binData + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with binData + document: + a: 16 + binaryValueofA: "00001000" + query: + a: + $bitsAnySet: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + matches: false + min_server_version: 3.2 + +- name: existing binData field matches + document: + a: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + query: + a: + $bitsAnySet: 37 + matches: true + min_server_version: 3.2 + +- name: existing binData field does not match + document: + a: !ruby/object:BSON::Binary + data: !binary |- + IW== + type: :generic + query: + a: + $bitsAnySet: 20 + matches: false + min_server_version: 3.2 + +- name: existing field - matches with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAnySet: [0, 2] + matches: true + min_server_version: 3.2 + +- name: existing field - does not match with array + document: + a: 20 + binaryValueofA: "00010100" + query: + a: + $bitsAnySet: [0, 3] + matches: false + min_server_version: 3.2 + +- name: float condition representable as an integer + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: 35 + matches: true + min_server_version: 3.2 + +- name: float condition not representable as an integer + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: 35.1 + error: true + min_server_version: 3.2 + +- name: string condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: hello + error: true + min_server_version: 3.2 + +- name: empty array condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: [] + matches: false + min_server_version: 3.2 + +- name: integer 0 condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: 0 + matches: false + min_server_version: 3.2 + +- name: negative integer condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: -1 + error: true + min_server_version: 3.2 + +- name: negative float condition + document: + a: 54 + binaryValueofA: "00110110" + query: + a: + $bitsAnySet: -1.0 + error: true + min_server_version: 3.2