mongory 0.7.3-aarch64-linux-musl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mongory might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +364 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +107 -0
- data/SUBMODULE_INTEGRATION.md +325 -0
- data/docs/advanced_usage.md +40 -0
- data/docs/clang_bridge.md +69 -0
- data/docs/field_names.md +30 -0
- data/docs/migration.md +30 -0
- data/docs/performance.md +61 -0
- data/examples/README.md +41 -0
- data/examples/benchmark-rails.rb +52 -0
- data/examples/benchmark.rb +184 -0
- data/ext/mongory_ext/extconf.rb +91 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/array.h +122 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/config.h +161 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/error.h +79 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/memory_pool.h +95 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/table.h +127 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/foundations/value.h +175 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core/matchers/matcher.h +76 -0
- data/ext/mongory_ext/mongory-core/include/mongory-core.h +12 -0
- data/ext/mongory_ext/mongory-core/src/foundations/array.c +287 -0
- data/ext/mongory_ext/mongory-core/src/foundations/array_private.h +19 -0
- data/ext/mongory_ext/mongory-core/src/foundations/config.c +270 -0
- data/ext/mongory_ext/mongory-core/src/foundations/config_private.h +48 -0
- data/ext/mongory_ext/mongory-core/src/foundations/error.c +38 -0
- data/ext/mongory_ext/mongory-core/src/foundations/memory_pool.c +298 -0
- data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.c +65 -0
- data/ext/mongory_ext/mongory-core/src/foundations/string_buffer.h +49 -0
- data/ext/mongory_ext/mongory-core/src/foundations/table.c +498 -0
- data/ext/mongory_ext/mongory-core/src/foundations/utils.c +210 -0
- data/ext/mongory_ext/mongory-core/src/foundations/utils.h +70 -0
- data/ext/mongory_ext/mongory-core/src/foundations/value.c +500 -0
- data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.c +164 -0
- data/ext/mongory_ext/mongory-core/src/matchers/array_record_matcher.h +47 -0
- data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.c +122 -0
- data/ext/mongory_ext/mongory-core/src/matchers/base_matcher.h +100 -0
- data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.c +217 -0
- data/ext/mongory_ext/mongory-core/src/matchers/compare_matcher.h +83 -0
- data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.c +573 -0
- data/ext/mongory_ext/mongory-core/src/matchers/composite_matcher.h +125 -0
- data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.c +147 -0
- data/ext/mongory_ext/mongory-core/src/matchers/existance_matcher.h +48 -0
- data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.c +124 -0
- data/ext/mongory_ext/mongory-core/src/matchers/external_matcher.h +46 -0
- data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.c +126 -0
- data/ext/mongory_ext/mongory-core/src/matchers/inclusion_matcher.h +46 -0
- data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.c +314 -0
- data/ext/mongory_ext/mongory-core/src/matchers/literal_matcher.h +97 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher.c +252 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.c +79 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_explainable.h +23 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.c +60 -0
- data/ext/mongory_ext/mongory-core/src/matchers/matcher_traversable.h +23 -0
- data/ext/mongory_ext/mongory_ext.c +683 -0
- data/lib/generators/mongory/install/install_generator.rb +42 -0
- data/lib/generators/mongory/install/templates/initializer.rb.erb +83 -0
- data/lib/generators/mongory/matcher/matcher_generator.rb +56 -0
- data/lib/generators/mongory/matcher/templates/matcher.rb.erb +92 -0
- data/lib/generators/mongory/matcher/templates/matcher_spec.rb.erb +17 -0
- data/lib/mongory/c_query_builder.rb +44 -0
- data/lib/mongory/converters/abstract_converter.rb +111 -0
- data/lib/mongory/converters/condition_converter.rb +64 -0
- data/lib/mongory/converters/converted.rb +81 -0
- data/lib/mongory/converters/data_converter.rb +37 -0
- data/lib/mongory/converters/key_converter.rb +87 -0
- data/lib/mongory/converters/value_converter.rb +52 -0
- data/lib/mongory/converters.rb +8 -0
- data/lib/mongory/matchers/abstract_matcher.rb +219 -0
- data/lib/mongory/matchers/abstract_multi_matcher.rb +124 -0
- data/lib/mongory/matchers/and_matcher.rb +72 -0
- data/lib/mongory/matchers/array_record_matcher.rb +93 -0
- data/lib/mongory/matchers/elem_match_matcher.rb +55 -0
- data/lib/mongory/matchers/eq_matcher.rb +46 -0
- data/lib/mongory/matchers/every_matcher.rb +56 -0
- data/lib/mongory/matchers/exists_matcher.rb +53 -0
- data/lib/mongory/matchers/field_matcher.rb +147 -0
- data/lib/mongory/matchers/gt_matcher.rb +41 -0
- data/lib/mongory/matchers/gte_matcher.rb +41 -0
- data/lib/mongory/matchers/hash_condition_matcher.rb +62 -0
- data/lib/mongory/matchers/in_matcher.rb +68 -0
- data/lib/mongory/matchers/literal_matcher.rb +121 -0
- data/lib/mongory/matchers/lt_matcher.rb +41 -0
- data/lib/mongory/matchers/lte_matcher.rb +41 -0
- data/lib/mongory/matchers/ne_matcher.rb +38 -0
- data/lib/mongory/matchers/nin_matcher.rb +68 -0
- data/lib/mongory/matchers/not_matcher.rb +40 -0
- data/lib/mongory/matchers/or_matcher.rb +68 -0
- data/lib/mongory/matchers/present_matcher.rb +55 -0
- data/lib/mongory/matchers/regex_matcher.rb +80 -0
- data/lib/mongory/matchers/size_matcher.rb +54 -0
- data/lib/mongory/matchers.rb +176 -0
- data/lib/mongory/mongoid.rb +19 -0
- data/lib/mongory/query_builder.rb +257 -0
- data/lib/mongory/query_matcher.rb +93 -0
- data/lib/mongory/query_operator.rb +28 -0
- data/lib/mongory/rails.rb +15 -0
- data/lib/mongory/utils/context.rb +48 -0
- data/lib/mongory/utils/debugger.rb +125 -0
- data/lib/mongory/utils/rails_patch.rb +22 -0
- data/lib/mongory/utils/singleton_builder.rb +31 -0
- data/lib/mongory/utils.rb +76 -0
- data/lib/mongory/version.rb +5 -0
- data/lib/mongory.rb +123 -0
- data/lib/mongory_ext.so +0 -0
- data/mongory.gemspec +62 -0
- data/scripts/build_with_core.sh +292 -0
- data/sig/mongory.rbs +4 -0
- metadata +159 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# ElemMatchMatcher implements the logic for Mongo-style `$elemMatch`.
|
6
|
+
#
|
7
|
+
# It is used to determine if *any* element in an array matches the given condition.
|
8
|
+
#
|
9
|
+
# This matcher delegates element-wise comparison to HashConditionMatcher,
|
10
|
+
# allowing nested conditions to be applied recursively.
|
11
|
+
#
|
12
|
+
# Typically used internally by ArrayRecordMatcher when dealing with
|
13
|
+
# non-indexed hash-style subconditions.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = ElemMatchMatcher.build({ status: 'active' })
|
17
|
+
# matcher.match?([{ status: 'inactive' }, { status: 'active' }]) #=> true
|
18
|
+
#
|
19
|
+
# @see HashConditionMatcher
|
20
|
+
class ElemMatchMatcher < HashConditionMatcher
|
21
|
+
# Creates a raw Proc that performs the element matching operation.
|
22
|
+
# The Proc checks if any element in the array matches the condition.
|
23
|
+
#
|
24
|
+
# @return [Proc] a Proc that performs the element matching operation
|
25
|
+
def raw_proc
|
26
|
+
super_proc = super
|
27
|
+
need_convert = @context.need_convert
|
28
|
+
data_converter = Mongory.data_converter
|
29
|
+
|
30
|
+
Proc.new do |collection|
|
31
|
+
collection.any? do |record|
|
32
|
+
record = data_converter.convert(record) if need_convert
|
33
|
+
super_proc.call(record)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def priority
|
39
|
+
3 + super
|
40
|
+
end
|
41
|
+
|
42
|
+
# Ensures the condition is a Hash.
|
43
|
+
#
|
44
|
+
# @raise [Mongory::TypeError] if the condition is not a Hash
|
45
|
+
# @return [void]
|
46
|
+
def check_validity!
|
47
|
+
raise TypeError, '$elemMatch needs a Hash.' unless @condition.is_a?(Hash)
|
48
|
+
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
register(:elem_match, '$elemMatch', ElemMatchMatcher)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# EqMatcher matches values using the equality operator `==`.
|
6
|
+
#
|
7
|
+
# It inherits from AbstractMatcher and defines its operator as `:==`.
|
8
|
+
#
|
9
|
+
# Used for conditions like:
|
10
|
+
# - { age: { '$eq' => 30 } }
|
11
|
+
# - { name: "Alice" } (implicit fallback)
|
12
|
+
#
|
13
|
+
# This matcher supports any Ruby object that implements `#==`.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = EqMatcher.build(42)
|
17
|
+
# matcher.match?(42) #=> true
|
18
|
+
# matcher.match?("42") #=> false
|
19
|
+
#
|
20
|
+
# @note This matcher is also used as the fallback for non-operator literal values,
|
21
|
+
# such as `{ name: "Alice" }`, when no other specialized matcher is applicable.
|
22
|
+
#
|
23
|
+
# @note Equality behavior depends on how `==` is implemented for the given objects.
|
24
|
+
#
|
25
|
+
# @see AbstractMatcher
|
26
|
+
class EqMatcher < AbstractMatcher
|
27
|
+
# Creates a raw Proc that performs the equality check.
|
28
|
+
# The Proc uses the `==` operator to compare values.
|
29
|
+
#
|
30
|
+
# @return [Proc] a Proc that performs the equality check
|
31
|
+
def raw_proc
|
32
|
+
condition = @condition
|
33
|
+
|
34
|
+
Proc.new do |record|
|
35
|
+
record == condition
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def priority
|
40
|
+
1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
register(:eq, '$eq', EqMatcher)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# EveryMatcher implements the logic for Mongo-style `$every` which is not really support in MongoDB.
|
6
|
+
#
|
7
|
+
# It is used to determine if *all* element in an array matches the given condition.
|
8
|
+
#
|
9
|
+
# This matcher delegates element-wise comparison to HashConditionMatcher,
|
10
|
+
# allowing nested conditions to be applied recursively.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = EveryMatcher.build({ status: 'active' })
|
14
|
+
# matcher.match?([{ status: 'inactive' }, { status: 'active' }]) #=> false
|
15
|
+
#
|
16
|
+
# @see HashConditionMatcher
|
17
|
+
class EveryMatcher < HashConditionMatcher
|
18
|
+
# Creates a raw Proc that performs the element matching operation.
|
19
|
+
# The Proc checks if all elements in the array match the condition.
|
20
|
+
#
|
21
|
+
# @return [Proc] A proc that performs element matching with context awareness
|
22
|
+
# @note The proc includes error handling and context-based record conversion
|
23
|
+
def raw_proc
|
24
|
+
super_proc = super
|
25
|
+
need_convert = @context.need_convert
|
26
|
+
data_converter = Mongory.data_converter
|
27
|
+
|
28
|
+
Proc.new do |collection|
|
29
|
+
next false unless collection.is_a?(Array)
|
30
|
+
next false if collection.empty?
|
31
|
+
|
32
|
+
collection.all? do |record|
|
33
|
+
record = data_converter.convert(record) if need_convert
|
34
|
+
super_proc.call(record)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def priority
|
40
|
+
3 + super
|
41
|
+
end
|
42
|
+
|
43
|
+
# Ensures the condition is a Hash.
|
44
|
+
#
|
45
|
+
# @raise [Mongory::TypeError] if the condition is not a Hash
|
46
|
+
# @return [void]
|
47
|
+
def check_validity!
|
48
|
+
raise TypeError, '$every needs a Hash.' unless @condition.is_a?(Hash)
|
49
|
+
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
register(:every, '$every', EveryMatcher)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# ExistsMatcher implements the `$exists` operator, which checks whether a key exists.
|
6
|
+
#
|
7
|
+
# It transforms the presence (or absence) of a field into a boolean value,
|
8
|
+
# then compares it to the condition using the `==` operator.
|
9
|
+
#
|
10
|
+
# This matcher ensures the condition is strictly a boolean (`true` or `false`).
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = ExistsMatcher.build(true)
|
14
|
+
# matcher.match?(42) #=> true
|
15
|
+
# matcher.match?(KEY_NOT_FOUND) #=> false
|
16
|
+
#
|
17
|
+
# matcher = ExistsMatcher.build(false)
|
18
|
+
# matcher.match?(KEY_NOT_FOUND) #=> true
|
19
|
+
#
|
20
|
+
# @see AbstractMatcher
|
21
|
+
class ExistsMatcher < AbstractMatcher
|
22
|
+
# Creates a raw Proc that performs the existence check.
|
23
|
+
# The Proc checks if the record exists and compares it to the condition.
|
24
|
+
#
|
25
|
+
# @return [Proc] A proc that performs existence check with error handling
|
26
|
+
def raw_proc
|
27
|
+
condition = @condition
|
28
|
+
|
29
|
+
Proc.new do |record|
|
30
|
+
# Check if the record is nil or KEY_NOT_FOUND
|
31
|
+
# and compare it to the condition.
|
32
|
+
(record != KEY_NOT_FOUND) == condition
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def priority
|
37
|
+
2
|
38
|
+
end
|
39
|
+
|
40
|
+
# Ensures that the condition value is a valid boolean.
|
41
|
+
#
|
42
|
+
# @raise [TypeError] if condition is not true or false
|
43
|
+
# @return [void]
|
44
|
+
def check_validity!
|
45
|
+
return if [true, false].include?(@condition)
|
46
|
+
|
47
|
+
raise TypeError, "$exists needs a boolean, but got #{@condition.inspect}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
register(:exists, '$exists', ExistsMatcher)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# FieldMatcher handles field-level matching by extracting and comparing field values.
|
6
|
+
#
|
7
|
+
# This matcher is responsible for:
|
8
|
+
# 1. Extracting field values from records using dot notation
|
9
|
+
# 2. Converting extracted values if needed
|
10
|
+
# 3. Delegating the actual comparison to a submatcher
|
11
|
+
#
|
12
|
+
# It supports:
|
13
|
+
# - Hash records with string/symbol keys
|
14
|
+
# - Array records with numeric indices
|
15
|
+
# - Objects that respond to `[]`
|
16
|
+
#
|
17
|
+
# @example Basic field matching
|
18
|
+
# matcher = FieldMatcher.build('age', 30)
|
19
|
+
# matcher.match?({ 'age' => 30 }) #=> true
|
20
|
+
# matcher.match?({ age: 30 }) #=> true
|
21
|
+
#
|
22
|
+
# @see LiteralMatcher
|
23
|
+
class FieldMatcher < LiteralMatcher
|
24
|
+
# A list of classes that should never be used for value digging.
|
25
|
+
# These typically respond to `#[]` but are semantically invalid for this context.
|
26
|
+
CLASSES_NOT_ALLOW_TO_DIG = [
|
27
|
+
::String,
|
28
|
+
::Integer,
|
29
|
+
::Proc,
|
30
|
+
::Method,
|
31
|
+
::MatchData,
|
32
|
+
::Thread,
|
33
|
+
::Symbol
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
# Initializes a new field matcher.
|
37
|
+
#
|
38
|
+
# @param field [String, Symbol] the field to match against
|
39
|
+
# @param condition [Object] the condition to match with
|
40
|
+
# @param context [Context] the query context
|
41
|
+
def initialize(field, condition, context: Context.new)
|
42
|
+
@field = field
|
43
|
+
super(condition, context: context)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Creates a raw Proc that performs the field matching operation.
|
47
|
+
# The Proc extracts the field value and delegates to the submatcher.
|
48
|
+
#
|
49
|
+
# This method first ensures the record is structurally eligible for field extraction—
|
50
|
+
# it must be a Hash, Array, or respond to `[]`. If the structure does not allow for
|
51
|
+
# field access (e.g., nil, primitive values, or unsupported types), the match returns false.
|
52
|
+
#
|
53
|
+
# The field value is then extracted using the following rules:
|
54
|
+
# - If the record is a Hash, it attempts to fetch using the field key,
|
55
|
+
# falling back to symbolized key if needed.
|
56
|
+
# - If the record is an Array, it fetches by index.
|
57
|
+
# - If the record does not support `[]` or is disallowed for dig operations,
|
58
|
+
# the match returns false immediately.
|
59
|
+
#
|
60
|
+
# Once the value is extracted, it is passed through the data converter
|
61
|
+
# and matched against the condition via the superclass.
|
62
|
+
#
|
63
|
+
# @example Matching a Hash with a nil field value
|
64
|
+
# matcher = Mongory::QueryMatcher.new(a: nil)
|
65
|
+
# matcher.match?({ a: nil }) # => true
|
66
|
+
#
|
67
|
+
# @example Record is nil (structure not diggable)
|
68
|
+
# matcher = Mongory::QueryMatcher.new(a: nil)
|
69
|
+
# matcher.match?(nil) # => false
|
70
|
+
#
|
71
|
+
# @example Matching against an Array by index
|
72
|
+
# matcher = Mongory::QueryMatcher.new(0 => /abc/)
|
73
|
+
# matcher.match?(['abcdef']) # => true
|
74
|
+
#
|
75
|
+
# @example Hash with symbol key, matcher uses string key
|
76
|
+
# matcher = Mongory::QueryMatcher.new('a' => 123)
|
77
|
+
# matcher.match?({ a: 123 }) # => true
|
78
|
+
|
79
|
+
# Creates a raw Proc that performs the field-based matching operation.
|
80
|
+
# The Proc extracts the field value and delegates matching to the superclass.
|
81
|
+
#
|
82
|
+
# @return [Proc] A proc that performs field-based matching with context awareness
|
83
|
+
# @note The proc handles field extraction and delegates matching to the superclass
|
84
|
+
def raw_proc
|
85
|
+
super_proc = super
|
86
|
+
field = @field
|
87
|
+
need_convert = @context.need_convert
|
88
|
+
data_converter = Mongory.data_converter
|
89
|
+
|
90
|
+
Proc.new do |record|
|
91
|
+
sub_record =
|
92
|
+
case record
|
93
|
+
when Hash
|
94
|
+
record.fetch(field) do
|
95
|
+
record.fetch(field.to_sym, KEY_NOT_FOUND)
|
96
|
+
end
|
97
|
+
when Array
|
98
|
+
record.fetch(field, KEY_NOT_FOUND)
|
99
|
+
when KEY_NOT_FOUND, *CLASSES_NOT_ALLOW_TO_DIG
|
100
|
+
next false
|
101
|
+
else
|
102
|
+
next false unless record.respond_to?(:[])
|
103
|
+
|
104
|
+
record[field]
|
105
|
+
end
|
106
|
+
|
107
|
+
sub_record = data_converter.convert(sub_record) if need_convert
|
108
|
+
super_proc.call(sub_record)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def priority
|
113
|
+
1 + super
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a unique key for this matcher, including the field name.
|
117
|
+
# Used for deduplication in multi-matchers.
|
118
|
+
#
|
119
|
+
# @return [String] a unique key for this matcher
|
120
|
+
# @see AbstractMultiMatcher#matchers
|
121
|
+
def uniq_key
|
122
|
+
super + "field:#{@field}"
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# Returns a single-line summary of the field matcher including the field and condition.
|
128
|
+
#
|
129
|
+
# @return [String] a formatted title for tree display
|
130
|
+
def tree_title
|
131
|
+
"Field: #{@field.inspect} to match: #{@condition.inspect}"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Custom display logic for debugging, including colored field highlighting.
|
135
|
+
#
|
136
|
+
# @param record [Object] the input record
|
137
|
+
# @param result [Boolean] match result
|
138
|
+
# @return [String] formatted debug string with highlighted field
|
139
|
+
def debug_display(record, result)
|
140
|
+
"#{self.class.name.split('::').last} #{colored_result(result)}, " \
|
141
|
+
"condition: #{@condition.inspect}, " \
|
142
|
+
"\e[30;47mfield: #{@field.inspect}\e[0m, " \
|
143
|
+
"record: #{record.inspect.gsub(@field.inspect, "\e[30;47m#{@field.inspect}\e[0m")}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# GtMatcher implements the `$gt` (greater than) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is strictly greater than the condition.
|
8
|
+
#
|
9
|
+
# Inherits core logic from AbstractMatcher, including
|
10
|
+
# error handling and optional preprocessing.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# matcher = GtMatcher.build(10)
|
14
|
+
# matcher.match?(15) #=> true
|
15
|
+
# matcher.match?(10) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractMatcher
|
18
|
+
class GtMatcher < AbstractMatcher
|
19
|
+
# Creates a raw Proc that performs the greater-than comparison.
|
20
|
+
# The Proc uses the `>` operator to compare values.
|
21
|
+
#
|
22
|
+
# @return [Proc] A proc that performs greater-than comparison with error handling
|
23
|
+
# @note The proc includes error handling for invalid comparisons
|
24
|
+
def raw_proc
|
25
|
+
condition = @condition
|
26
|
+
|
27
|
+
Proc.new do |record|
|
28
|
+
record > condition
|
29
|
+
rescue StandardError
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def priority
|
35
|
+
3
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
register(:gt, '$gt', GtMatcher)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# GteMatcher implements the `$gte` (greater than or equal) operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record is greater than or equal to the condition value.
|
8
|
+
#
|
9
|
+
# Inherits comparison logic and error safety from AbstractMatcher.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# matcher = GteMatcher.build(10)
|
13
|
+
# matcher.match?(10) #=> true
|
14
|
+
# matcher.match?(11) #=> true
|
15
|
+
# matcher.match?(9) #=> false
|
16
|
+
#
|
17
|
+
# @see AbstractMatcher
|
18
|
+
class GteMatcher < AbstractMatcher
|
19
|
+
# Creates a raw Proc that performs the greater-than-or-equal comparison.
|
20
|
+
# The Proc uses the `>=` operator to compare values.
|
21
|
+
#
|
22
|
+
# @return [Proc] A proc that performs greater-than-or-equal comparison with error handling
|
23
|
+
# @note The proc includes error handling for invalid comparisons
|
24
|
+
def raw_proc
|
25
|
+
condition = @condition
|
26
|
+
|
27
|
+
Proc.new do |record|
|
28
|
+
record >= condition
|
29
|
+
rescue StandardError
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def priority
|
35
|
+
3
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
register(:gte, '$gte', GteMatcher)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# HashConditionMatcher is responsible for handling field-level query conditions.
|
6
|
+
#
|
7
|
+
# It receives a Hash of key-value pairs and delegates each one to an appropriate matcher
|
8
|
+
# based on whether the key is a recognized operator or a data field path.
|
9
|
+
#
|
10
|
+
# Each subcondition is matched independently using the `:all?` strategy, meaning
|
11
|
+
# all subconditions must match for the entire HashConditionMatcher to succeed.
|
12
|
+
# For empty conditions, it returns true (using TRUE_PROC).
|
13
|
+
#
|
14
|
+
# This matcher plays a central role in dispatching symbolic query conditions
|
15
|
+
# to the appropriate field or operator matcher.
|
16
|
+
#
|
17
|
+
# @example Basic field matching
|
18
|
+
# matcher = HashConditionMatcher.build({ age: { :$gt => 30 }, active: true })
|
19
|
+
# matcher.match?(record) #=> true only if all subconditions match
|
20
|
+
#
|
21
|
+
# @example Empty conditions
|
22
|
+
# matcher = HashConditionMatcher.build({})
|
23
|
+
# matcher.match?(record) #=> true (uses TRUE_PROC)
|
24
|
+
#
|
25
|
+
# @see AbstractMultiMatcher
|
26
|
+
class HashConditionMatcher < AbstractMultiMatcher
|
27
|
+
enable_unwrap!
|
28
|
+
|
29
|
+
# Creates a raw Proc that performs the hash condition matching operation.
|
30
|
+
# The Proc combines all submatcher Procs and returns true only if all match.
|
31
|
+
# For empty conditions, returns TRUE_PROC.
|
32
|
+
#
|
33
|
+
# @return [Proc] a Proc that performs the hash condition matching operation
|
34
|
+
def raw_proc
|
35
|
+
combine_procs_with_and(*matchers.map(&:to_proc))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the list of matchers for each key-value pair in the condition.
|
39
|
+
#
|
40
|
+
# For each pair:
|
41
|
+
# - If the key is a registered operator, uses the corresponding matcher
|
42
|
+
# - Otherwise, wraps the value in a FieldMatcher for field path matching
|
43
|
+
#
|
44
|
+
# @return [Array<AbstractMatcher>] List of matchers for each condition
|
45
|
+
define_instance_cache_method(:matchers) do
|
46
|
+
@condition.map do |key, value|
|
47
|
+
if (matcher_class = Matchers.lookup(key))
|
48
|
+
matcher_class.build(value, context: @context)
|
49
|
+
else
|
50
|
+
FieldMatcher.build(key, value, context: @context)
|
51
|
+
end
|
52
|
+
end.sort_by(&:priority)
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_validity!
|
56
|
+
return super if @condition.is_a?(Hash)
|
57
|
+
|
58
|
+
raise TypeError, 'condition needs a Hash.'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# InMatcher implements the `$in` operator.
|
6
|
+
#
|
7
|
+
# It checks whether the record matches any value in the condition array.
|
8
|
+
# If the record is an array, it succeeds if any item overlaps with the condition.
|
9
|
+
# If the record is a single value (including `nil`), it matches if it is included in the condition.
|
10
|
+
#
|
11
|
+
# @example Match single value
|
12
|
+
# matcher = InMatcher.build([1, 2, 3])
|
13
|
+
# matcher.match?(2) #=> true
|
14
|
+
# matcher.match?(5) #=> false
|
15
|
+
#
|
16
|
+
# @example Match nil
|
17
|
+
# matcher = InMatcher.build([nil])
|
18
|
+
# matcher.match?(nil) #=> true
|
19
|
+
#
|
20
|
+
# @example Match with array
|
21
|
+
# matcher = InMatcher.build([2, 4])
|
22
|
+
# matcher.match?([1, 2, 3]) #=> true
|
23
|
+
# matcher.match?([5, 6]) #=> false
|
24
|
+
#
|
25
|
+
# @see AbstractMatcher
|
26
|
+
class InMatcher < AbstractMatcher
|
27
|
+
def self.build(condition, *args)
|
28
|
+
return super unless condition.is_a?(Range)
|
29
|
+
|
30
|
+
end_op = condition.exclude_end? ? '$lt' : '$lte'
|
31
|
+
head, tail = [condition.first, condition.last].sort
|
32
|
+
AndMatcher.build([{ '$gte' => head }, { end_op => tail }], *args)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a raw Proc that performs the in-matching operation.
|
36
|
+
# The Proc checks if any element of the record is in the condition array.
|
37
|
+
#
|
38
|
+
# @return [Proc] a Proc that performs the in-matching operation
|
39
|
+
def raw_proc
|
40
|
+
condition = Set.new(@condition)
|
41
|
+
|
42
|
+
Proc.new do |record|
|
43
|
+
if record.is_a?(Array)
|
44
|
+
is_present?(condition & record)
|
45
|
+
else
|
46
|
+
condition.include?(record)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def priority
|
52
|
+
1 + Math.log(@condition.size + 1, 1.5)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Ensures the condition is an array or range.
|
56
|
+
#
|
57
|
+
# @raise [TypeError] if condition is not an array nor a range
|
58
|
+
# @return [void]
|
59
|
+
def check_validity!
|
60
|
+
return if @condition.is_a?(Array)
|
61
|
+
|
62
|
+
raise TypeError, '$in needs an array or range'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
register(:in, '$in', InMatcher)
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# LiteralMatcher handles direct value comparison with special array handling.
|
6
|
+
#
|
7
|
+
# This matcher is used when a condition is a literal value (not an operator).
|
8
|
+
# It handles both direct equality comparison and array-record scenarios.
|
9
|
+
#
|
10
|
+
# For array records:
|
11
|
+
# - Uses ArrayRecordMatcher to check if any element matches
|
12
|
+
# For non-array records:
|
13
|
+
# - Uses appropriate matcher based on condition type (Hash, Regexp, nil, etc.)
|
14
|
+
#
|
15
|
+
# @example Basic equality matching
|
16
|
+
# matcher = LiteralMatcher.build(42)
|
17
|
+
# matcher.match?(42) #=> true
|
18
|
+
# matcher.match?([42, 43]) #=> true (array contains 42)
|
19
|
+
#
|
20
|
+
# @example Regexp matching
|
21
|
+
# matcher = LiteralMatcher.build(/foo/)
|
22
|
+
# matcher.match?("foo") #=> true
|
23
|
+
# matcher.match?(["foobar"]) #=> true
|
24
|
+
#
|
25
|
+
# @example Hash condition matching
|
26
|
+
# matcher = LiteralMatcher.build({ '$gt' => 10 })
|
27
|
+
# matcher.match?(15) #=> true
|
28
|
+
# matcher.match?([5, 15]) #=> true
|
29
|
+
#
|
30
|
+
# @see AbstractMatcher
|
31
|
+
# @see ArrayRecordMatcher
|
32
|
+
class LiteralMatcher < AbstractMatcher
|
33
|
+
# Creates a raw Proc that performs the literal matching operation.
|
34
|
+
# The Proc handles both array and non-array records appropriately.
|
35
|
+
#
|
36
|
+
# @return [Proc] a Proc that performs the literal matching operation
|
37
|
+
def raw_proc
|
38
|
+
array_record_proc = nil
|
39
|
+
dispatched_proc = dispatched_matcher.to_proc
|
40
|
+
|
41
|
+
Proc.new do |record|
|
42
|
+
if record.is_a?(Array)
|
43
|
+
array_record_proc ||= array_record_matcher.to_proc
|
44
|
+
array_record_proc.call(record)
|
45
|
+
else
|
46
|
+
dispatched_proc.call(record)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def priority
|
52
|
+
1 + dispatched_matcher.priority
|
53
|
+
end
|
54
|
+
|
55
|
+
# Selects and returns the appropriate matcher instance for a given literal condition.
|
56
|
+
#
|
57
|
+
# This method analyzes the type of the raw condition (e.g., Hash, Regexp, nil)
|
58
|
+
# and returns a dedicated matcher instance accordingly:
|
59
|
+
#
|
60
|
+
# - Hash → dispatches to `HashConditionMatcher`
|
61
|
+
# - Regexp → dispatches to `RegexMatcher`
|
62
|
+
# - nil → dispatches to an `OrMatcher` that emulates MongoDB's `{ field: nil }` behavior
|
63
|
+
#
|
64
|
+
# For all other literal types, this method returns `EqMatcher`, and fallback equality matching will be used.
|
65
|
+
#
|
66
|
+
# This matcher is cached after the first invocation using `define_instance_cache_method`
|
67
|
+
# to avoid unnecessary re-instantiation.
|
68
|
+
#
|
69
|
+
# @see Mongory::Matchers::HashConditionMatcher
|
70
|
+
# @see Mongory::Matchers::RegexMatcher
|
71
|
+
# @see Mongory::Matchers::OrMatcher
|
72
|
+
# @see Mongory::Matchers::EqMatcher
|
73
|
+
# @return [AbstractMatcher] the matcher used for non-array literal values
|
74
|
+
# @!method dispatched_matcher
|
75
|
+
define_matcher(:dispatched) do
|
76
|
+
case @condition
|
77
|
+
when Hash
|
78
|
+
HashConditionMatcher.build(@condition, context: @context)
|
79
|
+
when Regexp
|
80
|
+
RegexMatcher.build(@condition, context: @context)
|
81
|
+
when nil
|
82
|
+
OrMatcher.build([
|
83
|
+
{ '$exists' => false },
|
84
|
+
{ '$eq' => nil }
|
85
|
+
], context: @context)
|
86
|
+
else
|
87
|
+
EqMatcher.build(@condition, context: @context)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Lazily defines the collection matcher for array records.
|
92
|
+
#
|
93
|
+
# @see ArrayRecordMatcher
|
94
|
+
# @return [ArrayRecordMatcher] the matcher used to match array-type records
|
95
|
+
# @!method array_record_matcher
|
96
|
+
define_matcher(:array_record) do
|
97
|
+
ArrayRecordMatcher.build(@condition, context: @context)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Validates the nested condition matcher, if applicable.
|
101
|
+
#
|
102
|
+
# @return [void]
|
103
|
+
def check_validity!
|
104
|
+
dispatched_matcher.check_validity!
|
105
|
+
end
|
106
|
+
|
107
|
+
# Outputs the matcher tree by selecting either collection or condition matcher.
|
108
|
+
# Delegates `render_tree` to whichever submatcher was active.
|
109
|
+
#
|
110
|
+
# @param prefix [String] the prefix string for tree rendering
|
111
|
+
# @param is_last [Boolean] whether this is the last node in the tree
|
112
|
+
# @return [void]
|
113
|
+
def render_tree(prefix = '', is_last: true)
|
114
|
+
super
|
115
|
+
|
116
|
+
target_matcher = @array_record_matcher || dispatched_matcher
|
117
|
+
target_matcher.render_tree("#{prefix}#{is_last ? ' ' : '│ '}", is_last: true)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|