mongory 0.3.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +37 -7
- data/examples/benchmark-rails.rb +52 -0
- data/examples/benchmark.rb +79 -18
- data/lib/generators/mongory/matcher/matcher_generator.rb +1 -1
- data/lib/mongory/converters/abstract_converter.rb +22 -32
- data/lib/mongory/converters/condition_converter.rb +7 -4
- data/lib/mongory/converters/data_converter.rb +18 -7
- data/lib/mongory/converters/key_converter.rb +43 -19
- data/lib/mongory/converters/value_converter.rb +24 -19
- data/lib/mongory/matchers/abstract_matcher.rb +94 -32
- data/lib/mongory/matchers/abstract_multi_matcher.rb +16 -45
- data/lib/mongory/matchers/and_matcher.rb +38 -10
- data/lib/mongory/matchers/array_record_matcher.rb +54 -28
- data/lib/mongory/matchers/elem_match_matcher.rb +13 -9
- data/lib/mongory/matchers/eq_matcher.rb +12 -7
- data/lib/mongory/matchers/every_matcher.rb +20 -9
- data/lib/mongory/matchers/exists_matcher.rb +15 -14
- data/lib/mongory/matchers/field_matcher.rb +58 -38
- data/lib/mongory/matchers/gt_matcher.rb +15 -7
- data/lib/mongory/matchers/gte_matcher.rb +15 -7
- data/lib/mongory/matchers/hash_condition_matcher.rb +47 -25
- data/lib/mongory/matchers/in_matcher.rb +12 -10
- data/lib/mongory/matchers/literal_matcher.rb +42 -48
- data/lib/mongory/matchers/lt_matcher.rb +15 -7
- data/lib/mongory/matchers/lte_matcher.rb +15 -7
- data/lib/mongory/matchers/ne_matcher.rb +12 -7
- data/lib/mongory/matchers/nin_matcher.rb +12 -9
- data/lib/mongory/matchers/not_matcher.rb +9 -5
- data/lib/mongory/matchers/or_matcher.rb +42 -13
- data/lib/mongory/matchers/present_matcher.rb +14 -15
- data/lib/mongory/matchers/regex_matcher.rb +37 -22
- data/lib/mongory/matchers.rb +0 -1
- data/lib/mongory/query_builder.rb +88 -26
- data/lib/mongory/query_matcher.rb +39 -12
- data/lib/mongory/query_operator.rb +1 -1
- data/lib/mongory/utils/context.rb +41 -0
- data/lib/mongory/utils/debugger.rb +6 -4
- data/lib/mongory/utils.rb +1 -0
- data/lib/mongory/version.rb +1 -1
- data/lib/mongory.rb +3 -3
- data/mongory.gemspec +3 -3
- metadata +9 -10
- data/lib/mongory/matchers/README.md +0 -57
- data/lib/mongory/matchers/abstract_operator_matcher.rb +0 -46
- data/lib/mongory-rb.rb +0 -3
@@ -5,7 +5,8 @@ module Mongory
|
|
5
5
|
# OrMatcher implements the `$or` logical operator.
|
6
6
|
#
|
7
7
|
# It evaluates an array of subconditions and returns true
|
8
|
-
# if *any one* of them matches.
|
8
|
+
# if *any one* of them matches. For empty conditions, it returns false
|
9
|
+
# (using FALSE_PROC).
|
9
10
|
#
|
10
11
|
# Each subcondition is handled by a HashConditionMatcher with conversion disabled,
|
11
12
|
# since the parent matcher already manages data conversion.
|
@@ -20,24 +21,52 @@ module Mongory
|
|
20
21
|
# ])
|
21
22
|
# matcher.match?(record) #=> true if either condition matches
|
22
23
|
#
|
24
|
+
# @example Empty conditions
|
25
|
+
# matcher = OrMatcher.build([])
|
26
|
+
# matcher.match?(record) #=> false (uses FALSE_PROC)
|
27
|
+
#
|
23
28
|
# @see AbstractMultiMatcher
|
24
29
|
class OrMatcher < AbstractMultiMatcher
|
25
30
|
enable_unwrap!
|
26
|
-
|
27
|
-
#
|
28
|
-
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# @return [
|
32
|
-
def
|
33
|
-
|
31
|
+
|
32
|
+
# Creates a raw Proc that performs the or-matching operation.
|
33
|
+
# The Proc combines all submatcher Procs and returns true if any match.
|
34
|
+
# For empty conditions, returns FALSE_PROC.
|
35
|
+
#
|
36
|
+
# @return [Proc] a Proc that performs the or-matching operation
|
37
|
+
def raw_proc
|
38
|
+
return FALSE_PROC if matchers.empty?
|
39
|
+
|
40
|
+
combine_procs(*matchers.map(&:to_proc))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Recursively combines multiple matcher procs with OR logic.
|
44
|
+
# This method optimizes the combination of multiple matchers by building
|
45
|
+
# a balanced tree of OR operations.
|
46
|
+
#
|
47
|
+
# @param left [Proc] The left matcher proc to combine
|
48
|
+
# @param rest [Array<Proc>] The remaining matcher procs to combine
|
49
|
+
# @return [Proc] A new proc that combines all matchers with OR logic
|
50
|
+
# @example
|
51
|
+
# combine_procs(proc1, proc2, proc3)
|
52
|
+
# #=> proc { |record| proc1.call(record) || proc2.call(record) || proc3.call(record) }
|
53
|
+
def combine_procs(left, *rest)
|
54
|
+
return left if rest.empty?
|
55
|
+
|
56
|
+
right = combine_procs(*rest)
|
57
|
+
Proc.new do |record|
|
58
|
+
left.call(record) || right.call(record)
|
59
|
+
end
|
34
60
|
end
|
35
61
|
|
36
|
-
#
|
62
|
+
# Builds an array of matchers from the subconditions.
|
63
|
+
# Each subcondition is wrapped in a HashConditionMatcher.
|
37
64
|
#
|
38
|
-
# @return [
|
39
|
-
|
40
|
-
|
65
|
+
# @return [Array<AbstractMatcher>] array of submatchers
|
66
|
+
define_instance_cache_method(:matchers) do
|
67
|
+
@condition.map do |condition|
|
68
|
+
HashConditionMatcher.build(condition, context: @context)
|
69
|
+
end
|
41
70
|
end
|
42
71
|
|
43
72
|
# Ensures the condition is an array of hashes.
|
@@ -20,22 +20,19 @@ module Mongory
|
|
20
20
|
# matcher = PresentMatcher.build(false)
|
21
21
|
# matcher.match?(nil) #=> true
|
22
22
|
#
|
23
|
-
# @see
|
24
|
-
class PresentMatcher <
|
25
|
-
#
|
26
|
-
#
|
23
|
+
# @see AbstractMatcher
|
24
|
+
class PresentMatcher < AbstractMatcher
|
25
|
+
# Creates a raw Proc that performs the presence check.
|
26
|
+
# The Proc checks if the record's presence matches the condition.
|
27
27
|
#
|
28
|
-
# @
|
29
|
-
|
30
|
-
|
31
|
-
is_present?(super)
|
32
|
-
end
|
28
|
+
# @return [Proc] A proc that performs presence check
|
29
|
+
def raw_proc
|
30
|
+
condition = @condition
|
33
31
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
:==
|
32
|
+
Proc.new do |record|
|
33
|
+
record = nil if record == KEY_NOT_FOUND
|
34
|
+
is_present?(record) == condition
|
35
|
+
end
|
39
36
|
end
|
40
37
|
|
41
38
|
# Ensures that the condition value is a boolean.
|
@@ -43,7 +40,9 @@ module Mongory
|
|
43
40
|
# @raise [TypeError] if condition is not true or false
|
44
41
|
# @return [void]
|
45
42
|
def check_validity!
|
46
|
-
|
43
|
+
return if [true, false].include?(@condition)
|
44
|
+
|
45
|
+
raise TypeError, '$present needs a boolean'
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
@@ -12,41 +12,56 @@ module Mongory
|
|
12
12
|
# If a string is provided instead of a Regexp, it will be converted via `Regexp.new(...)`.
|
13
13
|
# This ensures consistent behavior for queries like `:field.regex => "foo"` and `:field.regex => /foo/`.
|
14
14
|
#
|
15
|
-
# @example
|
16
|
-
# matcher = RegexMatcher.build(
|
15
|
+
# @example Basic regex matching
|
16
|
+
# matcher = RegexMatcher.build(/^foo/)
|
17
17
|
# matcher.match?('foobar') #=> true
|
18
18
|
# matcher.match?('barfoo') #=> false
|
19
19
|
#
|
20
|
-
# @example
|
21
|
-
# RegexMatcher.build(/admin/i)
|
20
|
+
# @example Case-insensitive matching
|
21
|
+
# matcher = RegexMatcher.build(/admin/i)
|
22
|
+
# matcher.match?("ADMIN") #=> true
|
22
23
|
#
|
23
|
-
# @example
|
24
|
-
#
|
24
|
+
# @example String pattern
|
25
|
+
# matcher = RegexMatcher.build("^foo")
|
26
|
+
# matcher.match?("foobar") #=> true
|
25
27
|
#
|
26
|
-
# @
|
27
|
-
#
|
28
|
-
|
29
|
-
|
28
|
+
# @example Non-string input
|
29
|
+
# matcher = RegexMatcher.build(/\d+/)
|
30
|
+
# matcher.match?(123) #=> false (not a string)
|
31
|
+
#
|
32
|
+
# @see AbstractMatcher
|
33
|
+
class RegexMatcher < AbstractMatcher
|
34
|
+
# Initializes the matcher with a regex pattern.
|
35
|
+
# Converts string patterns to Regexp objects.
|
30
36
|
#
|
31
|
-
# @
|
32
|
-
|
33
|
-
|
37
|
+
# @param condition [String, Regexp] the regex pattern to match against
|
38
|
+
# @param context [Context] the query context
|
39
|
+
# @raise [TypeError] if condition is not a string or Regexp
|
40
|
+
def initialize(condition, context: Context.new)
|
41
|
+
super
|
42
|
+
@condition = Regexp.new(condition) if condition.is_a?(String)
|
34
43
|
end
|
35
44
|
|
36
|
-
#
|
37
|
-
#
|
45
|
+
# Creates a raw Proc that performs the regex matching operation.
|
46
|
+
# The Proc checks if the record is a string that matches the pattern.
|
47
|
+
# Returns false for non-string inputs or if the match fails.
|
38
48
|
#
|
39
|
-
# @
|
40
|
-
|
41
|
-
|
42
|
-
|
49
|
+
# @return [Proc] a Proc that performs regex matching
|
50
|
+
def raw_proc
|
51
|
+
condition = @condition
|
52
|
+
|
53
|
+
Proc.new do |record|
|
54
|
+
next false unless record.is_a?(String)
|
43
55
|
|
44
|
-
|
56
|
+
record.match?(condition)
|
57
|
+
rescue StandardError
|
58
|
+
false
|
59
|
+
end
|
45
60
|
end
|
46
61
|
|
47
|
-
# Ensures the condition is a
|
62
|
+
# Ensures the condition is a valid regex pattern (Regexp or String).
|
48
63
|
#
|
49
|
-
# @raise [TypeError] if condition is not a string
|
64
|
+
# @raise [TypeError] if condition is not a string or Regexp
|
50
65
|
# @return [void]
|
51
66
|
def check_validity!
|
52
67
|
return if @condition.is_a?(Regexp)
|
data/lib/mongory/matchers.rb
CHANGED
@@ -153,7 +153,6 @@ end
|
|
153
153
|
|
154
154
|
require_relative 'matchers/abstract_matcher'
|
155
155
|
require_relative 'matchers/abstract_multi_matcher'
|
156
|
-
require_relative 'matchers/abstract_operator_matcher'
|
157
156
|
require_relative 'matchers/literal_matcher'
|
158
157
|
require_relative 'matchers/hash_condition_matcher'
|
159
158
|
require_relative 'matchers/and_matcher'
|
@@ -10,12 +10,19 @@ module Mongory
|
|
10
10
|
#
|
11
11
|
# Internally it compiles all conditions and invokes `QueryMatcher`.
|
12
12
|
#
|
13
|
-
# @example
|
13
|
+
# @example Basic query
|
14
14
|
# records.mongory
|
15
15
|
# .where(:age.gte => 18)
|
16
16
|
# .or({ :name => /J/ }, { :name.eq => 'Bob' })
|
17
17
|
# .limit(2)
|
18
18
|
# .to_a
|
19
|
+
#
|
20
|
+
# @example Complex query
|
21
|
+
# records.mongory
|
22
|
+
# .where(:status => 'active')
|
23
|
+
# .not(:age.lt => 18)
|
24
|
+
# .any_of({ :role => 'admin' }, { :role => 'moderator' })
|
25
|
+
# .pluck(:name, :email)
|
19
26
|
class QueryBuilder
|
20
27
|
include ::Enumerable
|
21
28
|
include Utils
|
@@ -23,44 +30,68 @@ module Mongory
|
|
23
30
|
# Initializes a new query builder with the given record set.
|
24
31
|
#
|
25
32
|
# @param records [Enumerable] the collection to query against
|
26
|
-
def initialize(records)
|
33
|
+
def initialize(records, context: Utils::Context.new)
|
27
34
|
@records = records
|
35
|
+
@context = context
|
28
36
|
set_matcher
|
29
37
|
end
|
30
38
|
|
31
39
|
# Iterates through all records that match the current matcher.
|
40
|
+
# Uses the standard matcher implementation.
|
32
41
|
#
|
33
|
-
# @yieldparam record [Object]
|
34
|
-
# @return [Enumerator]
|
42
|
+
# @yieldparam record [Object] each matching record
|
43
|
+
# @return [Enumerator] if no block given
|
44
|
+
# @return [void] if block given
|
35
45
|
def each
|
36
46
|
return to_enum(:each) unless block_given?
|
37
47
|
|
38
48
|
@matcher.prepare_query
|
39
49
|
@records.each do |record|
|
50
|
+
@context.current_record = record
|
40
51
|
yield record if @matcher.match?(record)
|
41
52
|
end
|
42
53
|
end
|
43
54
|
|
55
|
+
# Iterates through all records that match the current matcher.
|
56
|
+
# Uses a compiled Proc for faster matching.
|
57
|
+
#
|
58
|
+
# @yieldparam record [Object] each matching record
|
59
|
+
# @return [Enumerator] if no block given
|
60
|
+
# @return [void] if block given
|
61
|
+
def fast
|
62
|
+
return to_enum(:fast) unless block_given?
|
63
|
+
|
64
|
+
@context.need_convert = false
|
65
|
+
@matcher.prepare_query
|
66
|
+
matcher_block = @matcher.to_proc
|
67
|
+
@records.each do |record|
|
68
|
+
yield record if matcher_block.call(record)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
44
72
|
# Adds a condition to filter records using the given condition.
|
73
|
+
# This is an alias for `and`.
|
45
74
|
#
|
46
|
-
# @param condition [Hash]
|
75
|
+
# @param condition [Hash] the condition to add
|
47
76
|
# @return [QueryBuilder] a new builder instance
|
48
77
|
def where(condition)
|
49
78
|
self.and(condition)
|
50
79
|
end
|
51
80
|
|
52
81
|
# Adds a negated condition to the current query.
|
82
|
+
# Wraps the condition in a `$not` operator.
|
53
83
|
#
|
54
|
-
# @param condition [Hash]
|
55
|
-
# @return [QueryBuilder]
|
84
|
+
# @param condition [Hash] the condition to negate
|
85
|
+
# @return [QueryBuilder] a new builder instance
|
56
86
|
def not(condition)
|
57
87
|
self.and('$not' => condition)
|
58
88
|
end
|
59
89
|
|
60
90
|
# Adds one or more conditions combined with `$and`.
|
91
|
+
# All conditions must match for a record to be included.
|
61
92
|
#
|
62
|
-
# @param conditions [Array<Hash>]
|
63
|
-
# @return [QueryBuilder]
|
93
|
+
# @param conditions [Array<Hash>] the conditions to add
|
94
|
+
# @return [QueryBuilder] a new builder instance
|
64
95
|
def and(*conditions)
|
65
96
|
dup_instance_exec do
|
66
97
|
add_conditions('$and', conditions)
|
@@ -68,9 +99,10 @@ module Mongory
|
|
68
99
|
end
|
69
100
|
|
70
101
|
# Adds one or more conditions combined with `$or`.
|
102
|
+
# Any condition can match for a record to be included.
|
71
103
|
#
|
72
|
-
# @param conditions [Array<Hash>]
|
73
|
-
# @return [QueryBuilder]
|
104
|
+
# @param conditions [Array<Hash>] the conditions to add
|
105
|
+
# @return [QueryBuilder] a new builder instance
|
74
106
|
def or(*conditions)
|
75
107
|
operator = '$or'
|
76
108
|
dup_instance_exec do
|
@@ -85,24 +117,34 @@ module Mongory
|
|
85
117
|
# Adds a `$or` query combined inside an `$and` block.
|
86
118
|
# This is a semantic alias for `.and('$or' => [...])`
|
87
119
|
#
|
88
|
-
# @param conditions [Array<Hash>]
|
89
|
-
# @return [QueryBuilder]
|
120
|
+
# @param conditions [Array<Hash>] the conditions to add
|
121
|
+
# @return [QueryBuilder] a new builder instance
|
90
122
|
def any_of(*conditions)
|
91
123
|
self.and('$or' => conditions)
|
92
124
|
end
|
93
125
|
|
126
|
+
# Adds an `$in` condition to the query.
|
127
|
+
# Matches records where the field value is in the given array.
|
128
|
+
#
|
129
|
+
# @param condition [Hash] the field and values to match
|
130
|
+
# @return [QueryBuilder] a new builder instance
|
94
131
|
def in(condition)
|
95
132
|
self.and(wrap_values_with_key(condition, '$in'))
|
96
133
|
end
|
97
134
|
|
135
|
+
# Adds a `$nin` condition to the query.
|
136
|
+
# Matches records where the field value is not in the given array.
|
137
|
+
#
|
138
|
+
# @param condition [Hash] the field and values to exclude
|
139
|
+
# @return [QueryBuilder] a new builder instance
|
98
140
|
def nin(condition)
|
99
141
|
self.and(wrap_values_with_key(condition, '$nin'))
|
100
142
|
end
|
101
143
|
|
102
144
|
# Limits the number of records returned by the query.
|
103
145
|
#
|
104
|
-
# @param count [Integer]
|
105
|
-
# @return [QueryBuilder]
|
146
|
+
# @param count [Integer] the maximum number of records to return
|
147
|
+
# @return [QueryBuilder] a new builder instance
|
106
148
|
def limit(count)
|
107
149
|
dup_instance_exec do
|
108
150
|
@records = take(count)
|
@@ -111,9 +153,10 @@ module Mongory
|
|
111
153
|
|
112
154
|
# Extracts selected fields from matching records.
|
113
155
|
#
|
114
|
-
# @param field [Symbol, String]
|
115
|
-
# @param fields [Array<Symbol, String>]
|
116
|
-
# @return [Array<Object
|
156
|
+
# @param field [Symbol, String] the first field to extract
|
157
|
+
# @param fields [Array<Symbol, String>] additional fields to extract
|
158
|
+
# @return [Array<Object>] array of single field values if one field given
|
159
|
+
# @return [Array<Array<Object>>] array of field value arrays if multiple fields given
|
117
160
|
def pluck(field, *fields)
|
118
161
|
if fields.empty?
|
119
162
|
map { |record| record[field] }
|
@@ -126,11 +169,24 @@ module Mongory
|
|
126
169
|
# Returns the raw parsed condition for this query.
|
127
170
|
#
|
128
171
|
# @return [Hash] the raw compiled condition
|
129
|
-
def
|
172
|
+
def raw_condition
|
130
173
|
@matcher.condition
|
131
174
|
end
|
132
175
|
|
133
|
-
|
176
|
+
# Creates a new query builder with additional context configuration.
|
177
|
+
#
|
178
|
+
# @param addon_context [Hash] Additional context configuration to merge
|
179
|
+
# @return [QueryBuilder] A new query builder instance with merged context
|
180
|
+
# @note Creates a new query builder with the current matcher's condition and merged context
|
181
|
+
# @example
|
182
|
+
# query.with_context(need_convert: false) #=> Returns a new query builder with conversion disabled
|
183
|
+
def with_context(addon_context = {})
|
184
|
+
dup_instance_exec do
|
185
|
+
@context = @context.dup
|
186
|
+
@context.config.merge!(addon_context)
|
187
|
+
set_matcher(@matcher.condition)
|
188
|
+
end
|
189
|
+
end
|
134
190
|
|
135
191
|
# Prints the internal matcher tree structure for the current query.
|
136
192
|
# Will output a human-readable visual tree of matchers.
|
@@ -148,8 +204,7 @@ module Mongory
|
|
148
204
|
# @private
|
149
205
|
# Duplicates the query and executes the block in context.
|
150
206
|
#
|
151
|
-
# @
|
152
|
-
# @return [QueryBuilder]
|
207
|
+
# @return [QueryBuilder] the modified duplicate
|
153
208
|
def dup_instance_exec(&block)
|
154
209
|
dup.tap do |obj|
|
155
210
|
obj.instance_exec(&block)
|
@@ -160,17 +215,18 @@ module Mongory
|
|
160
215
|
# Builds the internal matcher tree from a condition hash.
|
161
216
|
# Used to eagerly parse conditions to improve inspect/debug visibility.
|
162
217
|
#
|
163
|
-
# @param condition [Hash]
|
218
|
+
# @param condition [Hash] the condition to build the matcher from
|
164
219
|
# @return [void]
|
165
220
|
def set_matcher(condition = {})
|
166
|
-
@matcher = QueryMatcher.new(condition)
|
221
|
+
@matcher = QueryMatcher.new(condition, context: @context)
|
167
222
|
end
|
168
223
|
|
169
224
|
# @private
|
170
225
|
# Merges additional conditions into the matcher.
|
171
226
|
#
|
172
|
-
# @param key [String, Symbol]
|
173
|
-
# @param conditions [Array<Hash>]
|
227
|
+
# @param key [String, Symbol] the operator key (e.g. '$and', '$or')
|
228
|
+
# @param conditions [Array<Hash>] the conditions to add
|
229
|
+
# @return [void]
|
174
230
|
def add_conditions(key, conditions)
|
175
231
|
condition_dup = @matcher.condition.dup
|
176
232
|
condition_dup[key] ||= []
|
@@ -178,6 +234,12 @@ module Mongory
|
|
178
234
|
set_matcher(condition_dup)
|
179
235
|
end
|
180
236
|
|
237
|
+
# @private
|
238
|
+
# Wraps values in a condition hash with a given operator key.
|
239
|
+
#
|
240
|
+
# @param condition [Hash] the condition to transform
|
241
|
+
# @param key [String] the operator key to wrap with
|
242
|
+
# @return [Hash] the transformed condition
|
181
243
|
def wrap_values_with_key(condition, key)
|
182
244
|
condition.transform_values do |sub_condition|
|
183
245
|
{ key => sub_condition }
|
@@ -11,31 +11,58 @@ module Mongory
|
|
11
11
|
# Typically used internally by `QueryBuilder`.
|
12
12
|
#
|
13
13
|
# Conversion via Mongory.data_converter is applied to the record
|
14
|
+
# before matching to ensure consistent data types.
|
14
15
|
#
|
15
|
-
# @example
|
16
|
-
# matcher = QueryMatcher.
|
17
|
-
# matcher.match?(record)
|
16
|
+
# @example Basic matching
|
17
|
+
# matcher = QueryMatcher.new({ :age.gte => 18 })
|
18
|
+
# matcher.match?(record) # => true/false
|
19
|
+
#
|
20
|
+
# @example Complex condition
|
21
|
+
# matcher = QueryMatcher.new({
|
22
|
+
# :age.gte => 18,
|
23
|
+
# :$or => [
|
24
|
+
# { :name => /J/ },
|
25
|
+
# { :name.eq => 'Bob' }
|
26
|
+
# ]
|
27
|
+
# })
|
18
28
|
#
|
19
29
|
# @see Matchers::LiteralMatcher
|
20
30
|
# @see Converters::ConditionConverter
|
21
31
|
class QueryMatcher < Matchers::LiteralMatcher
|
32
|
+
# Initializes a new query matcher with the given condition.
|
33
|
+
# The condition is converted using Mongory.condition_converter
|
34
|
+
# before being passed to the parent matcher.
|
35
|
+
#
|
22
36
|
# @param condition [Hash<Symbol, Object>] a query condition using operator-style symbol keys,
|
23
37
|
# e.g. { :age.gt => 18 }, which will be parsed by `Mongory.condition_converter`.
|
24
|
-
|
25
|
-
|
38
|
+
# @param context [Context] The query context containing configuration and current record
|
39
|
+
# @option context [Hash] :config The query configuration
|
40
|
+
# @option context [Object] :current_record The current record being processed
|
41
|
+
# @option context [Boolean] :need_convert Whether the record needs to be converted
|
42
|
+
def initialize(condition, context: Utils::Context.new)
|
43
|
+
super(Mongory.condition_converter.convert(condition), context: context)
|
26
44
|
end
|
27
45
|
|
28
|
-
#
|
46
|
+
# Returns a Proc that can be used for fast matching.
|
47
|
+
# The Proc converts the record using Mongory.data_converter
|
48
|
+
# and delegates to the superclass's raw_proc.
|
29
49
|
#
|
30
|
-
# @
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
50
|
+
# @return [Proc] A proc that performs query matching with context awareness
|
51
|
+
# @note The proc includes error handling and context-based record conversion
|
52
|
+
def raw_proc
|
53
|
+
super_proc = super
|
54
|
+
need_convert = @context.need_convert
|
55
|
+
data_converter = Mongory.data_converter
|
56
|
+
|
57
|
+
Proc.new do |record|
|
58
|
+
record = data_converter.convert(record) if need_convert
|
59
|
+
super_proc.call(record)
|
60
|
+
rescue StandardError
|
61
|
+
false
|
62
|
+
end
|
35
63
|
end
|
36
64
|
|
37
65
|
# Renders the full matcher tree for the current query.
|
38
|
-
#
|
39
66
|
# This method is intended to be the public entry point for rendering
|
40
67
|
# the matcher tree. It does not accept any arguments and internally
|
41
68
|
# handles rendering via the configured pretty-print logic.
|
@@ -22,7 +22,7 @@ module Mongory
|
|
22
22
|
# @param other [Object] the value to match against
|
23
23
|
# @return [Hash] converted query condition
|
24
24
|
def __expr_part__(other, *)
|
25
|
-
|
25
|
+
{ @name => { @operator => other } }
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Utils
|
5
|
+
# Context is a utility class that provides a stable but mutatable
|
6
|
+
# shared context for the Mongory query builder. It holds the configuration
|
7
|
+
# and the current record being matcher tree processed.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# context = Mongory::Utils::Context.new(config)
|
11
|
+
# context.current_record = record
|
12
|
+
# context.config = new_config
|
13
|
+
#
|
14
|
+
# @attr [Config] config The configuration object for the context
|
15
|
+
# @attr [Record] current_record The current record being processed in the matcher tree
|
16
|
+
# @attr [Boolean] need_convert Whether the record needs to be converted before matching
|
17
|
+
class Context
|
18
|
+
attr_accessor :config, :current_record, :need_convert
|
19
|
+
|
20
|
+
# Initializes a new Context instance with the given configuration.
|
21
|
+
#
|
22
|
+
# @param config [Config] The configuration object for the context.
|
23
|
+
# @return [Context] A new Context instance.
|
24
|
+
def initialize(config = {})
|
25
|
+
@config = config
|
26
|
+
@current_record = nil
|
27
|
+
@need_convert = true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates a duplicate of the context with its own configuration.
|
31
|
+
#
|
32
|
+
# @return [Context] A new context instance with duplicated configuration
|
33
|
+
# @note The new context shares the same configuration object but has its own state
|
34
|
+
def dup
|
35
|
+
new_context = super
|
36
|
+
new_context.config = @config.dup
|
37
|
+
new_context
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -32,16 +32,18 @@ module Mongory
|
|
32
32
|
@trace_entries = []
|
33
33
|
end
|
34
34
|
|
35
|
-
# Enables debug mode by aliasing `
|
35
|
+
# Enables debug mode by aliasing `to_proc` to `debug_proc`.
|
36
36
|
# @return [void]
|
37
|
+
# @note Changes the behavior of to_proc to use debug_proc instead of cached_proc
|
37
38
|
def enable
|
38
|
-
Matchers::AbstractMatcher.alias_method :
|
39
|
+
Matchers::AbstractMatcher.alias_method :to_proc, :debug_proc
|
39
40
|
end
|
40
41
|
|
41
|
-
# Disables debug mode by restoring `
|
42
|
+
# Disables debug mode by restoring `to_proc` to `cached_proc`.
|
42
43
|
# @return [void]
|
44
|
+
# @note Restores the original behavior of to_proc
|
43
45
|
def disable
|
44
|
-
Matchers::AbstractMatcher.alias_method :
|
46
|
+
Matchers::AbstractMatcher.alias_method :to_proc, :cached_proc
|
45
47
|
end
|
46
48
|
|
47
49
|
# Wraps a matcher evaluation block with indentation control.
|
data/lib/mongory/utils.rb
CHANGED
data/lib/mongory/version.rb
CHANGED
data/lib/mongory.rb
CHANGED
@@ -58,21 +58,21 @@ module Mongory
|
|
58
58
|
#
|
59
59
|
# @return [Converters::DataConverter]
|
60
60
|
def self.data_converter
|
61
|
-
Converters::DataConverter.instance
|
61
|
+
@data_converter ||= Converters::DataConverter.instance
|
62
62
|
end
|
63
63
|
|
64
64
|
# Returns the condition converter instance.
|
65
65
|
#
|
66
66
|
# @return [Converters::ConditionConverter]
|
67
67
|
def self.condition_converter
|
68
|
-
Converters::ConditionConverter.instance
|
68
|
+
@condition_converter ||= Converters::ConditionConverter.instance
|
69
69
|
end
|
70
70
|
|
71
71
|
# Returns the debugger instance.
|
72
72
|
#
|
73
73
|
# @return [Utils::Debugger]
|
74
74
|
def self.debugger
|
75
|
-
Utils::Debugger.instance
|
75
|
+
@debugger ||= Utils::Debugger.instance
|
76
76
|
end
|
77
77
|
|
78
78
|
# Builds a new query over the given record set.
|
data/mongory.gemspec
CHANGED
@@ -10,15 +10,15 @@ Gem::Specification.new do |spec|
|
|
10
10
|
|
11
11
|
spec.summary = 'MongoDB-like in-memory query DSL for Ruby'
|
12
12
|
spec.description = 'A Mongo-like in-memory query DSL for Ruby'
|
13
|
-
spec.homepage = 'https://
|
13
|
+
spec.homepage = 'https://mongoryhq.github.io/mongory-rb/'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
spec.required_ruby_version = '>= 2.6.0'
|
16
16
|
|
17
17
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
18
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
19
19
|
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
-
spec.metadata['source_code_uri'] = 'https://github.com/
|
21
|
-
spec.metadata['changelog_uri'] = 'https://github.com/
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/mongoryhq/mongory-rb'
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/mongoryhq/mongory-rb/blob/main/CHANGELOG.md'
|
22
22
|
|
23
23
|
# Specify which files should be added to the gem when it is released.
|
24
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|