mongory 0.7.3-x86_64-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,257 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utils'
|
4
|
+
|
5
|
+
module Mongory
|
6
|
+
# QueryBuilder provides a Mongo-like in-memory query interface.
|
7
|
+
#
|
8
|
+
# It supports condition chaining (`where`, `or`, `not`),
|
9
|
+
# limiting, and plucking fields.
|
10
|
+
#
|
11
|
+
# Internally it compiles all conditions and invokes `QueryMatcher`.
|
12
|
+
#
|
13
|
+
# @example Basic query
|
14
|
+
# records.mongory
|
15
|
+
# .where(:age.gte => 18)
|
16
|
+
# .or({ :name => /J/ }, { :name.eq => 'Bob' })
|
17
|
+
# .limit(2)
|
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)
|
26
|
+
class QueryBuilder
|
27
|
+
include ::Enumerable
|
28
|
+
include Utils
|
29
|
+
|
30
|
+
# Initializes a new query builder with the given record set.
|
31
|
+
#
|
32
|
+
# @param records [Enumerable] the collection to query against
|
33
|
+
def initialize(records, context: Utils::Context.new)
|
34
|
+
@records = records
|
35
|
+
@context = context
|
36
|
+
set_matcher
|
37
|
+
end
|
38
|
+
|
39
|
+
# Iterates through all records that match the current matcher.
|
40
|
+
# Uses the standard matcher implementation.
|
41
|
+
#
|
42
|
+
# @yieldparam record [Object] each matching record
|
43
|
+
# @return [Enumerator] if no block given
|
44
|
+
# @return [void] if block given
|
45
|
+
def each
|
46
|
+
return to_enum(:each) unless block_given?
|
47
|
+
|
48
|
+
@matcher.prepare_query
|
49
|
+
@records.each do |record|
|
50
|
+
@context.current_record = record
|
51
|
+
yield record if @matcher.match?(record)
|
52
|
+
end
|
53
|
+
end
|
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
|
+
|
72
|
+
# Adds a condition to filter records using the given condition.
|
73
|
+
# This is an alias for `and`.
|
74
|
+
#
|
75
|
+
# @param condition [Hash] the condition to add
|
76
|
+
# @return [QueryBuilder] a new builder instance
|
77
|
+
def where(condition)
|
78
|
+
self.and(condition)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Adds a negated condition to the current query.
|
82
|
+
# Wraps the condition in a `$not` operator.
|
83
|
+
#
|
84
|
+
# @param condition [Hash] the condition to negate
|
85
|
+
# @return [QueryBuilder] a new builder instance
|
86
|
+
def not(condition)
|
87
|
+
self.and('$not' => condition)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Adds one or more conditions combined with `$and`.
|
91
|
+
# All conditions must match for a record to be included.
|
92
|
+
#
|
93
|
+
# @param conditions [Array<Hash>] the conditions to add
|
94
|
+
# @return [QueryBuilder] a new builder instance
|
95
|
+
def and(*conditions)
|
96
|
+
dup_instance_exec do
|
97
|
+
add_conditions('$and', conditions)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Adds one or more conditions combined with `$or`.
|
102
|
+
# Any condition can match for a record to be included.
|
103
|
+
#
|
104
|
+
# @param conditions [Array<Hash>] the conditions to add
|
105
|
+
# @return [QueryBuilder] a new builder instance
|
106
|
+
def or(*conditions)
|
107
|
+
operator = '$or'
|
108
|
+
dup_instance_exec do
|
109
|
+
if @matcher.condition.each_key.all? { |k| k == operator }
|
110
|
+
add_conditions(operator, conditions)
|
111
|
+
else
|
112
|
+
set_matcher(operator => [@matcher.condition.dup, *conditions])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Adds a `$or` query combined inside an `$and` block.
|
118
|
+
# This is a semantic alias for `.and('$or' => [...])`
|
119
|
+
#
|
120
|
+
# @param conditions [Array<Hash>] the conditions to add
|
121
|
+
# @return [QueryBuilder] a new builder instance
|
122
|
+
def any_of(*conditions)
|
123
|
+
self.and('$or' => conditions)
|
124
|
+
end
|
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
|
131
|
+
def in(condition)
|
132
|
+
self.and(wrap_values_with_key(condition, '$in'))
|
133
|
+
end
|
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
|
140
|
+
def nin(condition)
|
141
|
+
self.and(wrap_values_with_key(condition, '$nin'))
|
142
|
+
end
|
143
|
+
|
144
|
+
# Limits the number of records returned by the query.
|
145
|
+
#
|
146
|
+
# @param count [Integer] the maximum number of records to return
|
147
|
+
# @return [QueryBuilder] a new builder instance
|
148
|
+
def limit(count)
|
149
|
+
dup_instance_exec do
|
150
|
+
@records = take(count)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Extracts selected fields from matching records.
|
155
|
+
#
|
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
|
160
|
+
def pluck(field, *fields)
|
161
|
+
if fields.empty?
|
162
|
+
map { |record| record[field] }
|
163
|
+
else
|
164
|
+
fields.unshift(field)
|
165
|
+
map { |record| fields.map { |key| record[key] } }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the raw parsed condition for this query.
|
170
|
+
#
|
171
|
+
# @return [Hash] the raw compiled condition
|
172
|
+
def raw_condition
|
173
|
+
@matcher.condition
|
174
|
+
end
|
175
|
+
|
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
|
190
|
+
|
191
|
+
# Prints the internal matcher tree structure for the current query.
|
192
|
+
# Will output a human-readable visual tree of matchers.
|
193
|
+
# This is useful for debugging and visualizing complex conditions.
|
194
|
+
#
|
195
|
+
# @return [void]
|
196
|
+
def explain
|
197
|
+
@matcher.match?(@records.first)
|
198
|
+
@matcher.render_tree
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def c
|
203
|
+
return self unless defined?(Mongory::CMatcher)
|
204
|
+
|
205
|
+
c_builder = CQueryBuilder.new(@records, context: @context)
|
206
|
+
c_builder.send(:set_matcher, @matcher.condition)
|
207
|
+
c_builder
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
# @private
|
213
|
+
# Duplicates the query and executes the block in context.
|
214
|
+
#
|
215
|
+
# @return [QueryBuilder] the modified duplicate
|
216
|
+
def dup_instance_exec(&block)
|
217
|
+
dup.tap do |obj|
|
218
|
+
obj.instance_exec(&block)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# @private
|
223
|
+
# Builds the internal matcher tree from a condition hash.
|
224
|
+
# Used to eagerly parse conditions to improve inspect/debug visibility.
|
225
|
+
#
|
226
|
+
# @param condition [Hash] the condition to build the matcher from
|
227
|
+
# @return [void]
|
228
|
+
def set_matcher(condition = {})
|
229
|
+
@matcher = QueryMatcher.new(condition, context: @context)
|
230
|
+
end
|
231
|
+
|
232
|
+
# @private
|
233
|
+
# Merges additional conditions into the matcher.
|
234
|
+
#
|
235
|
+
# @param key [String, Symbol] the operator key (e.g. '$and', '$or')
|
236
|
+
# @param conditions [Array<Hash>] the conditions to add
|
237
|
+
# @return [void]
|
238
|
+
def add_conditions(key, conditions)
|
239
|
+
condition_dup = {}.merge!(@matcher.condition)
|
240
|
+
condition_dup[key] ||= []
|
241
|
+
condition_dup[key] += conditions
|
242
|
+
set_matcher(condition_dup)
|
243
|
+
end
|
244
|
+
|
245
|
+
# @private
|
246
|
+
# Wraps values in a condition hash with a given operator key.
|
247
|
+
#
|
248
|
+
# @param condition [Hash] the condition to transform
|
249
|
+
# @param key [String] the operator key to wrap with
|
250
|
+
# @return [Hash] the transformed condition
|
251
|
+
def wrap_values_with_key(condition, key)
|
252
|
+
condition.transform_values do |sub_condition|
|
253
|
+
{ key => sub_condition }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utils'
|
4
|
+
|
5
|
+
module Mongory
|
6
|
+
# The top-level matcher for compiled query conditions.
|
7
|
+
#
|
8
|
+
# Delegates to {Matchers::LiteralMatcher} after transforming input
|
9
|
+
# via {Converters::ConditionConverter}.
|
10
|
+
#
|
11
|
+
# Typically used internally by `QueryBuilder`.
|
12
|
+
#
|
13
|
+
# Conversion via Mongory.data_converter is applied to the record
|
14
|
+
# before matching to ensure consistent data types.
|
15
|
+
#
|
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
|
+
# })
|
28
|
+
#
|
29
|
+
# @see Matchers::LiteralMatcher
|
30
|
+
# @see Converters::ConditionConverter
|
31
|
+
class QueryMatcher < Matchers::HashConditionMatcher
|
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
|
+
#
|
36
|
+
# @param condition [Hash<Symbol, Object>] a query condition using operator-style symbol keys,
|
37
|
+
# e.g. { :age.gt => 18 }, which will be parsed by `Mongory.condition_converter`.
|
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)
|
44
|
+
end
|
45
|
+
|
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.
|
49
|
+
#
|
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
|
63
|
+
end
|
64
|
+
|
65
|
+
# Renders the full matcher tree for the current query.
|
66
|
+
# This method is intended to be the public entry point for rendering
|
67
|
+
# the matcher tree. It does not accept any arguments and internally
|
68
|
+
# handles rendering via the configured pretty-print logic.
|
69
|
+
#
|
70
|
+
# Subclasses or internal matchers should implement their own
|
71
|
+
# `#render_tree(prefix, is_last:)` for internal recursion.
|
72
|
+
#
|
73
|
+
# @return [void]
|
74
|
+
# @see Matchers::LiteralMatcher#render_tree
|
75
|
+
def render_tree
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
# Prepares the query for execution by ensuring all necessary matchers are initialized.
|
80
|
+
# This is called before query execution to avoid premature matcher tree construction.
|
81
|
+
#
|
82
|
+
# @return [void]
|
83
|
+
alias_method :prepare_query, :check_validity!
|
84
|
+
|
85
|
+
# Overrides the parent class's check_validity! to prevent premature matcher tree construction.
|
86
|
+
# This matcher does not require validation, so this is a no-op.
|
87
|
+
#
|
88
|
+
# @return [void]
|
89
|
+
def check_validity!
|
90
|
+
# No-op for this matcher
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
# Wrapper for symbol-based operator expressions.
|
5
|
+
#
|
6
|
+
# Used to support DSL like `:age.gt => 18` where `gt` maps to `$gt`.
|
7
|
+
# Converts into: `{ "age" => { "$gt" => 18 } }`
|
8
|
+
class QueryOperator
|
9
|
+
# Initializes a new query operator wrapper.
|
10
|
+
#
|
11
|
+
# @param name [String] the original field name
|
12
|
+
# @param operator [String] the Mongo-style operator (e.g., '$gt')
|
13
|
+
def initialize(name, operator)
|
14
|
+
@name = name
|
15
|
+
@operator = operator
|
16
|
+
end
|
17
|
+
|
18
|
+
# Converts the operator and value into a condition hash.
|
19
|
+
#
|
20
|
+
# Typically called by the key converter.
|
21
|
+
#
|
22
|
+
# @param other [Object] the value to match against
|
23
|
+
# @return [Hash] converted query condition
|
24
|
+
def __expr_part__(other, *)
|
25
|
+
{ @name => { @operator => other } }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require_relative 'utils/rails_patch'
|
5
|
+
|
6
|
+
module Mongory
|
7
|
+
# @see Utils::RailsPatch
|
8
|
+
class Railtie < Rails::Railtie
|
9
|
+
initializer 'mongory.patch_utils' do
|
10
|
+
[Mongory::Utils, *Mongory::Utils.included_classes].each do |klass|
|
11
|
+
klass.prepend(Mongory::Utils::RailsPatch)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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
|
+
|
40
|
+
def to_hash
|
41
|
+
{
|
42
|
+
config: @config,
|
43
|
+
need_convert: @need_convert
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Utils
|
5
|
+
# Debugger provides an internal tracing system for query matching.
|
6
|
+
# It tracks matcher evaluation in a tree structure and provides tree-like format output.
|
7
|
+
#
|
8
|
+
# It captures each matcher evaluation with indentation and visualizes the hierarchy,
|
9
|
+
# helping developers understand nested matcher flows (e.g. `$and`, `$or`, etc).
|
10
|
+
#
|
11
|
+
# Usage:
|
12
|
+
# Debugger.instance.with_indent { ... } # wraps around matcher logic
|
13
|
+
# Debugger.instance.display # prints trace tree after execution
|
14
|
+
#
|
15
|
+
# Note:
|
16
|
+
# The trace is recorded in post-order (leaf nodes enter first),
|
17
|
+
# and `reorder_traces_for_display` is used to transform it into a
|
18
|
+
# pre-order format suitable for display.
|
19
|
+
#
|
20
|
+
# This class is a singleton and should be accessed via `Debugger.instance`.
|
21
|
+
#
|
22
|
+
# @example Enable debugging
|
23
|
+
# Debugger.instance.enable
|
24
|
+
# Mongory::QueryBuilder.new(...).filter(...)
|
25
|
+
#
|
26
|
+
class Debugger < SingletonBuilder
|
27
|
+
include Singleton
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
super(self.class.name)
|
31
|
+
@indent_level = -1
|
32
|
+
@trace_entries = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Enables debug mode by aliasing `to_proc` to `debug_proc`.
|
36
|
+
# @return [void]
|
37
|
+
# @note Changes the behavior of to_proc to use debug_proc instead of cached_proc
|
38
|
+
def enable
|
39
|
+
Matchers::AbstractMatcher.alias_method :to_proc, :debug_proc
|
40
|
+
end
|
41
|
+
|
42
|
+
# Disables debug mode by restoring `to_proc` to `cached_proc`.
|
43
|
+
# @return [void]
|
44
|
+
# @note Restores the original behavior of to_proc
|
45
|
+
def disable
|
46
|
+
Matchers::AbstractMatcher.alias_method :to_proc, :cached_proc
|
47
|
+
end
|
48
|
+
|
49
|
+
# Wraps a matcher evaluation block with indentation control.
|
50
|
+
#
|
51
|
+
# It increments the internal indent level before the block,
|
52
|
+
# and decrements it after. The yielded block's return value
|
53
|
+
# is used as trace content and pushed onto the trace_entries.
|
54
|
+
#
|
55
|
+
# @yieldreturn [String] the trace content to log
|
56
|
+
# @return [Object] the result of the block
|
57
|
+
def with_indent
|
58
|
+
@indent_level += 1
|
59
|
+
display_string = yield
|
60
|
+
@trace_entries << TraceEntry.new(display_string, @indent_level)
|
61
|
+
ensure
|
62
|
+
@indent_level -= 1
|
63
|
+
end
|
64
|
+
|
65
|
+
# Prints the visualized trace tree to STDOUT.
|
66
|
+
#
|
67
|
+
# This processes the internal trace_entries (post-order) into
|
68
|
+
# a structured pre-order list that represents nested matcher evaluation.
|
69
|
+
# @return [void]
|
70
|
+
def display
|
71
|
+
reorder_traces_for_display(@trace_entries).each do |trace|
|
72
|
+
puts trace.formatted
|
73
|
+
end
|
74
|
+
|
75
|
+
@trace_entries.clear
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
# Recursively reorders trace lines by indentation level to produce a
|
80
|
+
# display-friendly structure where parents precede children.
|
81
|
+
#
|
82
|
+
# The original trace is built in post-order (leaf first), and this method
|
83
|
+
# transforms it into a pre-order structure suitable for tree display.
|
84
|
+
#
|
85
|
+
# @param traces [Array<TraceEntry>] the raw trace lines (leaf-first)
|
86
|
+
# @param level [Integer] current processing level
|
87
|
+
# @return [Array<TraceEntry>] reordered trace lines for display
|
88
|
+
def reorder_traces_for_display(traces, level = 0)
|
89
|
+
result = []
|
90
|
+
group = []
|
91
|
+
traces.each do |trace|
|
92
|
+
if trace.level == level
|
93
|
+
result << trace
|
94
|
+
result.concat reorder_traces_for_display(group, level + 1)
|
95
|
+
group = []
|
96
|
+
else
|
97
|
+
group << trace
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
result
|
102
|
+
end
|
103
|
+
|
104
|
+
def clear
|
105
|
+
# Clears the internal trace buffer.
|
106
|
+
# This is useful when re-running a query and avoiding stale trace output.
|
107
|
+
# @return [void]
|
108
|
+
@trace_entries.clear
|
109
|
+
end
|
110
|
+
|
111
|
+
# @private
|
112
|
+
# A trace entry representing a single matcher output at a given indentation level.
|
113
|
+
#
|
114
|
+
# @!attribute text
|
115
|
+
# @return [String] the string to be printed
|
116
|
+
# @!attribute level
|
117
|
+
# @return [Integer] the indentation level of this entry
|
118
|
+
TraceEntry = Struct.new(:text, :level) do
|
119
|
+
def formatted
|
120
|
+
"#{' ' * level}#{text}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Utils
|
5
|
+
# Only loaded when Rails is present
|
6
|
+
module RailsPatch
|
7
|
+
# Use Object#present? which defined in Rails.
|
8
|
+
# @param target[Object]
|
9
|
+
# @return [Boolean]
|
10
|
+
def is_present?(target)
|
11
|
+
target.present?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Use Object#blank? which defined in Rails.
|
15
|
+
# @param target[Object]
|
16
|
+
# @return [Boolean]
|
17
|
+
def is_blank?(target)
|
18
|
+
target.blank?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Utils
|
5
|
+
# A singleton placeholder object used to represent special sentinel values.
|
6
|
+
#
|
7
|
+
# Used in situations where `nil` is a valid value and cannot be used as a marker.
|
8
|
+
# Typically used for internal constants like `NOTHING` or `KEY_NOT_FOUND`.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# NOTHING = SingletonBuilder.new('NOTHING')
|
12
|
+
# value == NOTHING # => true if placeholder
|
13
|
+
class SingletonBuilder
|
14
|
+
# @param label [String] a human-readable label for the marker
|
15
|
+
def initialize(label, &block)
|
16
|
+
@label = label
|
17
|
+
instance_eval(&block) if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String] formatted label
|
21
|
+
def inspect
|
22
|
+
"#<#{@label}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String] formatted label
|
26
|
+
def to_s
|
27
|
+
"#<#{@label}>"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require_relative 'utils/singleton_builder'
|
5
|
+
require_relative 'utils/debugger'
|
6
|
+
require_relative 'utils/context'
|
7
|
+
|
8
|
+
module Mongory
|
9
|
+
# Utility helpers shared across Mongory internals.
|
10
|
+
#
|
11
|
+
# Includes blank checking, present checking,
|
12
|
+
# and class-level instance method caching.
|
13
|
+
module Utils
|
14
|
+
# When included, also extends the including class with ClassMethods.
|
15
|
+
# And record which class include this module.
|
16
|
+
#
|
17
|
+
# @param base [Class, Module]
|
18
|
+
def self.included(base)
|
19
|
+
base.extend(ClassMethods)
|
20
|
+
super
|
21
|
+
included_classes << base
|
22
|
+
end
|
23
|
+
|
24
|
+
# Where to record classes that include Utils.
|
25
|
+
#
|
26
|
+
# @return [Array]
|
27
|
+
def self.included_classes
|
28
|
+
@included_classes ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Checks if an object is "present".
|
32
|
+
# Inverse of {#is_blank?}.
|
33
|
+
#
|
34
|
+
# @param obj [Object]
|
35
|
+
# @return [Boolean]
|
36
|
+
def is_present?(obj)
|
37
|
+
!is_blank?(obj)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Determines whether an object is considered "blank".
|
41
|
+
# Nil, false, empty string/array/hash are blank.
|
42
|
+
#
|
43
|
+
# @param obj [Object]
|
44
|
+
# @return [Boolean]
|
45
|
+
def is_blank?(obj)
|
46
|
+
case obj
|
47
|
+
when false, nil
|
48
|
+
true
|
49
|
+
when Hash, Array, String, Set
|
50
|
+
obj.empty?
|
51
|
+
else
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Class-level methods injected via Utils.
|
57
|
+
module ClassMethods
|
58
|
+
# Defines a lazily-evaluated, memoized instance method.
|
59
|
+
#
|
60
|
+
# @param name [Symbol] the method name
|
61
|
+
# @yield block to compute the value
|
62
|
+
# @return [void]
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# define_instance_cache_method(:expensive_thing) { compute_something }
|
66
|
+
def define_instance_cache_method(name, &block)
|
67
|
+
instance_key = :"@#{name}"
|
68
|
+
define_method(name) do
|
69
|
+
return instance_variable_get(instance_key) if instance_variable_defined?(instance_key)
|
70
|
+
|
71
|
+
instance_variable_set(instance_key, instance_exec(&block))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|