mongory 0.3.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +84 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +246 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +517 -0
- data/Rakefile +12 -0
- data/examples/README.md +41 -0
- data/examples/benchmark.rb +44 -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/converters/abstract_converter.rb +122 -0
- data/lib/mongory/converters/condition_converter.rb +74 -0
- data/lib/mongory/converters/data_converter.rb +26 -0
- data/lib/mongory/converters/key_converter.rb +63 -0
- data/lib/mongory/converters/value_converter.rb +47 -0
- data/lib/mongory/converters.rb +7 -0
- data/lib/mongory/matchers/README.md +57 -0
- data/lib/mongory/matchers/abstract_matcher.rb +153 -0
- data/lib/mongory/matchers/abstract_multi_matcher.rb +109 -0
- data/lib/mongory/matchers/abstract_operator_matcher.rb +46 -0
- data/lib/mongory/matchers/and_matcher.rb +65 -0
- data/lib/mongory/matchers/array_record_matcher.rb +88 -0
- data/lib/mongory/matchers/elem_match_matcher.rb +47 -0
- data/lib/mongory/matchers/eq_matcher.rb +37 -0
- data/lib/mongory/matchers/every_matcher.rb +41 -0
- data/lib/mongory/matchers/exists_matcher.rb +48 -0
- data/lib/mongory/matchers/field_matcher.rb +123 -0
- data/lib/mongory/matchers/gt_matcher.rb +29 -0
- data/lib/mongory/matchers/gte_matcher.rb +29 -0
- data/lib/mongory/matchers/hash_condition_matcher.rb +55 -0
- data/lib/mongory/matchers/in_matcher.rb +52 -0
- data/lib/mongory/matchers/literal_matcher.rb +123 -0
- data/lib/mongory/matchers/lt_matcher.rb +29 -0
- data/lib/mongory/matchers/lte_matcher.rb +29 -0
- data/lib/mongory/matchers/ne_matcher.rb +29 -0
- data/lib/mongory/matchers/nin_matcher.rb +51 -0
- data/lib/mongory/matchers/not_matcher.rb +32 -0
- data/lib/mongory/matchers/or_matcher.rb +60 -0
- data/lib/mongory/matchers/present_matcher.rb +52 -0
- data/lib/mongory/matchers/regex_matcher.rb +61 -0
- data/lib/mongory/matchers.rb +176 -0
- data/lib/mongory/mongoid.rb +19 -0
- data/lib/mongory/query_builder.rb +187 -0
- data/lib/mongory/query_matcher.rb +66 -0
- data/lib/mongory/query_operator.rb +28 -0
- data/lib/mongory/rails.rb +15 -0
- data/lib/mongory/utils/debugger.rb +123 -0
- data/lib/mongory/utils/rails_patch.rb +22 -0
- data/lib/mongory/utils/singleton_builder.rb +31 -0
- data/lib/mongory/utils.rb +75 -0
- data/lib/mongory/version.rb +5 -0
- data/lib/mongory-rb.rb +3 -0
- data/lib/mongory.rb +116 -0
- data/mongory.gemspec +40 -0
- data/sig/mongory.rbs +4 -0
- metadata +108 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# PresentMatcher implements the `$present` operator.
|
6
|
+
#
|
7
|
+
# It returns true if the record value is considered "present"
|
8
|
+
# (i.e., not nil, not empty, not KEY_NOT_FOUND), and matches
|
9
|
+
# the expected boolean condition.
|
10
|
+
#
|
11
|
+
# This is similar to `$exists`, but evaluates truthiness
|
12
|
+
# of the value instead of mere existence.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# matcher = PresentMatcher.build(true)
|
16
|
+
# matcher.match?('hello') #=> true
|
17
|
+
# matcher.match?(nil) #=> false
|
18
|
+
# matcher.match?([]) #=> false
|
19
|
+
#
|
20
|
+
# matcher = PresentMatcher.build(false)
|
21
|
+
# matcher.match?(nil) #=> true
|
22
|
+
#
|
23
|
+
# @see AbstractOperatorMatcher
|
24
|
+
class PresentMatcher < AbstractOperatorMatcher
|
25
|
+
# Transforms the record into a boolean presence flag
|
26
|
+
# before applying comparison.
|
27
|
+
#
|
28
|
+
# @param record [Object] the original value
|
29
|
+
# @return [Boolean] whether the value is present
|
30
|
+
def preprocess(record)
|
31
|
+
is_present?(super)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Uses Ruby `==` to compare the presence result to the expected boolean.
|
35
|
+
#
|
36
|
+
# @return [Symbol] the equality operator
|
37
|
+
def operator
|
38
|
+
:==
|
39
|
+
end
|
40
|
+
|
41
|
+
# Ensures that the condition value is a boolean.
|
42
|
+
#
|
43
|
+
# @raise [TypeError] if condition is not true or false
|
44
|
+
# @return [void]
|
45
|
+
def check_validity!
|
46
|
+
raise TypeError, '$present needs a boolean' unless BOOLEAN_VALUES.include?(@condition)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
register(:present, '$present', PresentMatcher)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
module Matchers
|
5
|
+
# RegexMatcher implements the `$regex` operator and also handles raw Regexp values.
|
6
|
+
#
|
7
|
+
# This matcher checks whether a string record matches a regular expression.
|
8
|
+
# It supports both:
|
9
|
+
# - Explicit queries using `:field.regex => /pattern/i`
|
10
|
+
# - Implicit literal Regexp values like `{ field: /pattern/i }`
|
11
|
+
#
|
12
|
+
# If a string is provided instead of a Regexp, it will be converted via `Regexp.new(...)`.
|
13
|
+
# This ensures consistent behavior for queries like `:field.regex => "foo"` and `:field.regex => /foo/`.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = RegexMatcher.build('^foo')
|
17
|
+
# matcher.match?('foobar') #=> true
|
18
|
+
# matcher.match?('barfoo') #=> false
|
19
|
+
#
|
20
|
+
# @example Match with explicit regex
|
21
|
+
# RegexMatcher.build(/admin/i).match?("ADMIN") # => true
|
22
|
+
#
|
23
|
+
# @example Match via LiteralMatcher fallback
|
24
|
+
# LiteralMatcher.new(/admin/i).match("ADMIN") # => true
|
25
|
+
#
|
26
|
+
# @see LiteralMatcher
|
27
|
+
# @see Mongory::Matchers::AbstractOperatorMatcher
|
28
|
+
class RegexMatcher < AbstractOperatorMatcher
|
29
|
+
# Uses `:match?` as the operator to invoke on the record string.
|
30
|
+
#
|
31
|
+
# @return [Symbol] the match? method symbol
|
32
|
+
def operator
|
33
|
+
:match?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Ensures the record is a string before applying regex.
|
37
|
+
# If not, coerces to empty string to ensure match fails safely.
|
38
|
+
#
|
39
|
+
# @param record [Object] the raw input
|
40
|
+
# @return [String] a safe string to match against
|
41
|
+
def preprocess(record)
|
42
|
+
return '' unless record.is_a?(String)
|
43
|
+
|
44
|
+
record
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ensures the condition is a Regexp (strings are converted during initialization).
|
48
|
+
#
|
49
|
+
# @raise [TypeError] if condition is not a string
|
50
|
+
# @return [void]
|
51
|
+
def check_validity!
|
52
|
+
return if @condition.is_a?(Regexp)
|
53
|
+
return if @condition.is_a?(String)
|
54
|
+
|
55
|
+
raise TypeError, '$regex needs a Regexp or string'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
register(:regex, '$regex', RegexMatcher)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
# Provides matcher registration and operator-to-class lookup for query evaluation.
|
5
|
+
#
|
6
|
+
# This module is responsible for:
|
7
|
+
# - Mapping Mongo-style operators like "$gt" to matcher classes
|
8
|
+
# - Dynamically extending Symbol with query operator snippets (e.g., :age.gt)
|
9
|
+
# - Safely isolating symbol extension behind an explicit opt-in flag
|
10
|
+
#
|
11
|
+
# Matchers are registered using `Matchers.registry(method_sym, operator, klass)`
|
12
|
+
# and can be looked up via `Matchers.lookup(operator)`.
|
13
|
+
#
|
14
|
+
# Symbol snippets are only enabled if `Matchers.enable_symbol_snippets!` is called,
|
15
|
+
# preventing namespace pollution unless explicitly requested.
|
16
|
+
module Matchers
|
17
|
+
@operator_mapping = {}
|
18
|
+
@registries = []
|
19
|
+
|
20
|
+
# Registers a matcher class for a given operator and method symbol.
|
21
|
+
#
|
22
|
+
# @param method_sym [Symbol] the method name to be added to Symbol (e.g., :gt)
|
23
|
+
# @param operator [String] the Mongo-style operator (e.g., "$gt")
|
24
|
+
# @param klass [Class] the matcher class to associate with the operator
|
25
|
+
# @return [void]
|
26
|
+
# @raise [ArgumentError] if validations fail
|
27
|
+
def self.register(method_sym, operator, klass)
|
28
|
+
Validator.validate_method(method_sym)
|
29
|
+
Validator.validate_operator(operator)
|
30
|
+
Validator.validate_class(klass)
|
31
|
+
|
32
|
+
@operator_mapping[operator] = klass
|
33
|
+
registry = Registry.new(method_sym, operator)
|
34
|
+
@registries << registry
|
35
|
+
return unless @enable_symbol_snippets
|
36
|
+
|
37
|
+
registry.apply!
|
38
|
+
end
|
39
|
+
|
40
|
+
# Enables dynamic symbol snippet generation for registered operators.
|
41
|
+
# This defines methods like `:age.gt => QueryOperator.new(...)`.
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def self.enable_symbol_snippets!
|
45
|
+
@enable_symbol_snippets = true
|
46
|
+
@registries.each(&:apply!)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Retrieves the matcher class associated with a Mongo-style operator.
|
50
|
+
#
|
51
|
+
# @param operator [String]
|
52
|
+
# @return [Class, nil] the registered matcher class or nil if not found
|
53
|
+
def self.lookup(operator)
|
54
|
+
@operator_mapping[operator]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns all registered operator keys.
|
58
|
+
#
|
59
|
+
# @return [Array<String>]
|
60
|
+
def self.operators
|
61
|
+
@operator_mapping.keys
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.freeze
|
65
|
+
super
|
66
|
+
@operator_mapping.freeze
|
67
|
+
@registries.freeze
|
68
|
+
end
|
69
|
+
|
70
|
+
# @private
|
71
|
+
#
|
72
|
+
# Internal helper module used by `Matchers.registry` to validate matcher registration parameters.
|
73
|
+
#
|
74
|
+
# This includes:
|
75
|
+
# - Ensuring operators are valid Mongo-style strings (e.g., "$gt")
|
76
|
+
# - Verifying matcher class inheritance
|
77
|
+
# - Enforcing naming rules for symbol snippets (e.g., :gt, :not_match)
|
78
|
+
#
|
79
|
+
# These validations protect against incorrect matcher setup and prevent unsafe symbol definitions.
|
80
|
+
#
|
81
|
+
# @see Matchers.registry
|
82
|
+
module Validator
|
83
|
+
# Validates the given operator string.
|
84
|
+
# Ensures it matches the Mongo-style format like "$gt".
|
85
|
+
# Warns on duplicate registration.
|
86
|
+
#
|
87
|
+
# @param operator [String]
|
88
|
+
# @return [void]
|
89
|
+
# @raise [Mongory::TypeError] if operator format is invalid
|
90
|
+
def self.validate_operator(operator)
|
91
|
+
if Matchers.lookup(operator)
|
92
|
+
warn "Duplicate operator registration: #{operator} (#{Matchers.lookup(operator)} vs #{klass})"
|
93
|
+
end
|
94
|
+
|
95
|
+
return if operator.is_a?(String) && operator.match?(/^\$[a-z]+([A-Z][a-z]+)*$/)
|
96
|
+
|
97
|
+
raise Mongory::TypeError, "Operator must match /^\$[a-z]+([A-Z][a-z]*)*$/, but got #{operator.inspect}"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Validates the matcher class to ensure it is a subclass of AbstractMatcher.
|
101
|
+
#
|
102
|
+
# @param klass [Class]
|
103
|
+
# @return [void]
|
104
|
+
# @raise [Mongory::TypeError] if class is not valid
|
105
|
+
def self.validate_class(klass)
|
106
|
+
return if klass.is_a?(Class) && klass < AbstractMatcher
|
107
|
+
|
108
|
+
raise Mongory::TypeError, "Matcher class must be a subclass of AbstractMatcher, but got #{klass}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Validates the method symbol to ensure it is a valid lowercase underscore symbol (e.g., :gt, :not_match).
|
112
|
+
#
|
113
|
+
# @param method_sym [Symbol]
|
114
|
+
# @return [void]
|
115
|
+
# @raise [Mongory::TypeError] if symbol format is invalid
|
116
|
+
def self.validate_method(method_sym)
|
117
|
+
return if method_sym.is_a?(Symbol) && method_sym.match?(/^([a-z]+_)*[a-z]+$/)
|
118
|
+
|
119
|
+
raise Mongory::TypeError, "Method symbol must match /^([a-z]+_)*[a-z]+$/, but got #{method_sym.inspect}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# @private
|
124
|
+
#
|
125
|
+
# Internal helper representing a registration of an operator and its associated symbol snippet method.
|
126
|
+
# Used to delay method definition on Symbol until explicitly enabled.
|
127
|
+
#
|
128
|
+
# Each instance holds:
|
129
|
+
# - the method symbol (e.g., `:gt`)
|
130
|
+
# - the corresponding Mongo-style operator (e.g., `"$gt"`)
|
131
|
+
#
|
132
|
+
# These instances are collected and replayed upon calling `Matchers.enable_symbol_snippets!`.
|
133
|
+
#
|
134
|
+
# @!attribute method_sym
|
135
|
+
# @return [Symbol] the symbol method name (e.g., :in, :gt, :exists)
|
136
|
+
# @!attribute operator
|
137
|
+
# @return [String] the Mongo-style operator this snippet maps to (e.g., "$in")
|
138
|
+
Registry = Struct.new(:method_sym, :operator) do
|
139
|
+
# Defines a method on Symbol to support operator snippet expansion.
|
140
|
+
#
|
141
|
+
# @return [void]
|
142
|
+
def apply!
|
143
|
+
return if Symbol.method_defined?(method_sym)
|
144
|
+
|
145
|
+
operator = operator()
|
146
|
+
Symbol.define_method(method_sym) do
|
147
|
+
Mongory::QueryOperator.new(to_s, operator)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
require_relative 'matchers/abstract_matcher'
|
155
|
+
require_relative 'matchers/abstract_multi_matcher'
|
156
|
+
require_relative 'matchers/abstract_operator_matcher'
|
157
|
+
require_relative 'matchers/literal_matcher'
|
158
|
+
require_relative 'matchers/hash_condition_matcher'
|
159
|
+
require_relative 'matchers/and_matcher'
|
160
|
+
require_relative 'matchers/array_record_matcher'
|
161
|
+
require_relative 'matchers/elem_match_matcher'
|
162
|
+
require_relative 'matchers/every_matcher'
|
163
|
+
require_relative 'matchers/eq_matcher'
|
164
|
+
require_relative 'matchers/exists_matcher'
|
165
|
+
require_relative 'matchers/gt_matcher'
|
166
|
+
require_relative 'matchers/gte_matcher'
|
167
|
+
require_relative 'matchers/in_matcher'
|
168
|
+
require_relative 'matchers/field_matcher'
|
169
|
+
require_relative 'matchers/lt_matcher'
|
170
|
+
require_relative 'matchers/lte_matcher'
|
171
|
+
require_relative 'matchers/ne_matcher'
|
172
|
+
require_relative 'matchers/nin_matcher'
|
173
|
+
require_relative 'matchers/not_matcher'
|
174
|
+
require_relative 'matchers/or_matcher'
|
175
|
+
require_relative 'matchers/present_matcher'
|
176
|
+
require_relative 'matchers/regex_matcher'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongory
|
4
|
+
# Only loaded when Mongoid is present
|
5
|
+
module MongoidPatch
|
6
|
+
# Regist Mongoid operator key object into KeyConverter
|
7
|
+
# @see Converters::KeyConverter
|
8
|
+
# @return [void]
|
9
|
+
def self.patch!
|
10
|
+
kc = Mongory::Converters::KeyConverter.instance
|
11
|
+
# It's Mongoid built-in key operator that born from `:key.gt`
|
12
|
+
kc.register(::Mongoid::Criteria::Queryable::Key) do |v|
|
13
|
+
kc.convert(@name.to_s, @operator => v)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
patch!
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,187 @@
|
|
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
|
14
|
+
# records.mongory
|
15
|
+
# .where(:age.gte => 18)
|
16
|
+
# .or({ :name => /J/ }, { :name.eq => 'Bob' })
|
17
|
+
# .limit(2)
|
18
|
+
# .to_a
|
19
|
+
class QueryBuilder
|
20
|
+
include ::Enumerable
|
21
|
+
include Utils
|
22
|
+
|
23
|
+
# Initializes a new query builder with the given record set.
|
24
|
+
#
|
25
|
+
# @param records [Enumerable] the collection to query against
|
26
|
+
def initialize(records)
|
27
|
+
@records = records
|
28
|
+
set_matcher
|
29
|
+
end
|
30
|
+
|
31
|
+
# Iterates through all records that match the current matcher.
|
32
|
+
#
|
33
|
+
# @yieldparam record [Object]
|
34
|
+
# @return [Enumerator]
|
35
|
+
def each
|
36
|
+
return to_enum(:each) unless block_given?
|
37
|
+
|
38
|
+
@matcher.prepare_query
|
39
|
+
@records.each do |record|
|
40
|
+
yield record if @matcher.match?(record)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Adds a condition to filter records using the given condition.
|
45
|
+
#
|
46
|
+
# @param condition [Hash]
|
47
|
+
# @return [QueryBuilder] a new builder instance
|
48
|
+
def where(condition)
|
49
|
+
self.and(condition)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adds a negated condition to the current query.
|
53
|
+
#
|
54
|
+
# @param condition [Hash]
|
55
|
+
# @return [QueryBuilder]
|
56
|
+
def not(condition)
|
57
|
+
self.and('$not' => condition)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Adds one or more conditions combined with `$and`.
|
61
|
+
#
|
62
|
+
# @param conditions [Array<Hash>]
|
63
|
+
# @return [QueryBuilder]
|
64
|
+
def and(*conditions)
|
65
|
+
dup_instance_exec do
|
66
|
+
add_conditions('$and', conditions)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Adds one or more conditions combined with `$or`.
|
71
|
+
#
|
72
|
+
# @param conditions [Array<Hash>]
|
73
|
+
# @return [QueryBuilder]
|
74
|
+
def or(*conditions)
|
75
|
+
operator = '$or'
|
76
|
+
dup_instance_exec do
|
77
|
+
if @matcher.condition.each_key.all? { |k| k == operator }
|
78
|
+
add_conditions(operator, conditions)
|
79
|
+
else
|
80
|
+
set_matcher(operator => [@matcher.condition.dup, *conditions])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Adds a `$or` query combined inside an `$and` block.
|
86
|
+
# This is a semantic alias for `.and('$or' => [...])`
|
87
|
+
#
|
88
|
+
# @param conditions [Array<Hash>]
|
89
|
+
# @return [QueryBuilder]
|
90
|
+
def any_of(*conditions)
|
91
|
+
self.and('$or' => conditions)
|
92
|
+
end
|
93
|
+
|
94
|
+
def in(condition)
|
95
|
+
self.and(wrap_values_with_key(condition, '$in'))
|
96
|
+
end
|
97
|
+
|
98
|
+
def nin(condition)
|
99
|
+
self.and(wrap_values_with_key(condition, '$nin'))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Limits the number of records returned by the query.
|
103
|
+
#
|
104
|
+
# @param count [Integer]
|
105
|
+
# @return [QueryBuilder]
|
106
|
+
def limit(count)
|
107
|
+
dup_instance_exec do
|
108
|
+
@records = take(count)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Extracts selected fields from matching records.
|
113
|
+
#
|
114
|
+
# @param field [Symbol, String]
|
115
|
+
# @param fields [Array<Symbol, String>]
|
116
|
+
# @return [Array<Object>, Array<Array<Object>>]
|
117
|
+
def pluck(field, *fields)
|
118
|
+
if fields.empty?
|
119
|
+
map { |record| record[field] }
|
120
|
+
else
|
121
|
+
fields.unshift(field)
|
122
|
+
map { |record| fields.map { |key| record[key] } }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns the raw parsed condition for this query.
|
127
|
+
#
|
128
|
+
# @return [Hash] the raw compiled condition
|
129
|
+
def condition
|
130
|
+
@matcher.condition
|
131
|
+
end
|
132
|
+
|
133
|
+
alias_method :selector, :condition
|
134
|
+
|
135
|
+
# Prints the internal matcher tree structure for the current query.
|
136
|
+
# Will output a human-readable visual tree of matchers.
|
137
|
+
# This is useful for debugging and visualizing complex conditions.
|
138
|
+
#
|
139
|
+
# @return [void]
|
140
|
+
def explain
|
141
|
+
@matcher.match?(@records.first)
|
142
|
+
@matcher.render_tree
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
# @private
|
149
|
+
# Duplicates the query and executes the block in context.
|
150
|
+
#
|
151
|
+
# @yieldparam dup [QueryBuilder]
|
152
|
+
# @return [QueryBuilder]
|
153
|
+
def dup_instance_exec(&block)
|
154
|
+
dup.tap do |obj|
|
155
|
+
obj.instance_exec(&block)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# @private
|
160
|
+
# Builds the internal matcher tree from a condition hash.
|
161
|
+
# Used to eagerly parse conditions to improve inspect/debug visibility.
|
162
|
+
#
|
163
|
+
# @param condition [Hash]
|
164
|
+
# @return [void]
|
165
|
+
def set_matcher(condition = {})
|
166
|
+
@matcher = QueryMatcher.new(condition)
|
167
|
+
end
|
168
|
+
|
169
|
+
# @private
|
170
|
+
# Merges additional conditions into the matcher.
|
171
|
+
#
|
172
|
+
# @param key [String, Symbol]
|
173
|
+
# @param conditions [Array<Hash>]
|
174
|
+
def add_conditions(key, conditions)
|
175
|
+
condition_dup = @matcher.condition.dup
|
176
|
+
condition_dup[key] ||= []
|
177
|
+
condition_dup[key] += conditions
|
178
|
+
set_matcher(condition_dup)
|
179
|
+
end
|
180
|
+
|
181
|
+
def wrap_values_with_key(condition, key)
|
182
|
+
condition.transform_values do |sub_condition|
|
183
|
+
{ key => sub_condition }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,66 @@
|
|
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
|
+
#
|
15
|
+
# @example
|
16
|
+
# matcher = QueryMatcher.build({ :age.gte => 18 })
|
17
|
+
# matcher.match?(record)
|
18
|
+
#
|
19
|
+
# @see Matchers::LiteralMatcher
|
20
|
+
# @see Converters::ConditionConverter
|
21
|
+
class QueryMatcher < Matchers::LiteralMatcher
|
22
|
+
# @param condition [Hash<Symbol, Object>] a query condition using operator-style symbol keys,
|
23
|
+
# e.g. { :age.gt => 18 }, which will be parsed by `Mongory.condition_converter`.
|
24
|
+
def initialize(condition)
|
25
|
+
super(Mongory.condition_converter.convert(condition))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Matches the given record against the condition.
|
29
|
+
#
|
30
|
+
# @param record [Object] the raw input record (e.g., Hash or model object) to be matched.
|
31
|
+
# It will be converted internally using `Mongory.data_converter`.
|
32
|
+
# @return [Boolean] whether the record satisfies the condition
|
33
|
+
def match(record)
|
34
|
+
super(Mongory.data_converter.convert(record))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Renders the full matcher tree for the current query.
|
38
|
+
#
|
39
|
+
# This method is intended to be the public entry point for rendering
|
40
|
+
# the matcher tree. It does not accept any arguments and internally
|
41
|
+
# handles rendering via the configured pretty-print logic.
|
42
|
+
#
|
43
|
+
# Subclasses or internal matchers should implement their own
|
44
|
+
# `#render_tree(prefix, is_last:)` for internal recursion.
|
45
|
+
#
|
46
|
+
# @return [void]
|
47
|
+
# @see Matchers::LiteralMatcher#render_tree
|
48
|
+
def render_tree
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
# Prepares the query for execution by ensuring all necessary matchers are initialized.
|
53
|
+
# This is called before query execution to avoid premature matcher tree construction.
|
54
|
+
#
|
55
|
+
# @return [void]
|
56
|
+
alias_method :prepare_query, :check_validity!
|
57
|
+
|
58
|
+
# Overrides the parent class's check_validity! to prevent premature matcher tree construction.
|
59
|
+
# This matcher does not require validation, so this is a no-op.
|
60
|
+
#
|
61
|
+
# @return [void]
|
62
|
+
def check_validity!
|
63
|
+
# No-op for this matcher
|
64
|
+
end
|
65
|
+
end
|
66
|
+
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
|
+
Converters::KeyConverter.instance.convert(@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
|