Skip to content

Commit cab0350

Browse files
MONGOID-5314: Support aliases on index options partial_filter_expression, weights, and wildcard_projection (#5267)
Co-authored-by: shields <[email protected]>
1 parent 8f24baa commit cab0350

File tree

7 files changed

+553
-41
lines changed

7 files changed

+553
-41
lines changed

docs/reference/indexes.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ Indexes can be scoped to a specific database:
8282
index({ ssn: 1 }, { database: "users", unique: true, background: true })
8383
end
8484

85+
You may use aliased field names in index definitions. Field aliases
86+
will also be resolved on the following options: ``partial_filter_expression``,
87+
``weights``, ``wildcard_projection``.
88+
89+
.. code-block:: ruby
90+
91+
class Person
92+
include Mongoid::Document
93+
field :a, as: :age
94+
index({ age: 1 }, { partial_filter_expression: { age: { '$gte' => 20 } })
95+
end
96+
97+
.. note::
98+
99+
The expansion of field name aliases in index options such as
100+
``partial_filter_expression`` is performed according to the behavior of MongoDB
101+
server 6.0. Future server versions may change how they interpret these options,
102+
and Mongoid's functionality may not support such changes.
103+
85104
Mongoid can define indexes on "foreign key" fields for associations.
86105
This only works on the association macro that the foreign key is stored on:
87106

docs/release-notes/mongoid-9.0.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,29 @@ defaults to ``true``.
241241
When set to false, the older, inconsistent behavior is restored.
242242

243243

244+
Support Field Aliases on Index Options
245+
--------------------------------------
246+
247+
Support has been added to use aliased field names in the following options
248+
of the ``index`` macro: ``partial_filter_expression``, ``weights``,
249+
``wildcard_projection``.
250+
251+
.. code-block:: ruby
252+
253+
class Person
254+
include Mongoid::Document
255+
field :a, as: :age
256+
index({ age: 1 }, { partial_filter_expression: { age: { '$gte' => 20 } })
257+
end
258+
259+
.. note::
260+
261+
The expansion of field name aliases in index options such as
262+
``partial_filter_expression`` is performed according to the behavior of MongoDB
263+
server 6.0. Future server versions may change how they interpret these options,
264+
and Mongoid's functionality may not support such changes.
265+
266+
244267
Bug Fixes and Improvements
245268
--------------------------
246269

lib/mongoid/indexable/specification.rb

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def initialize(klass, key, opts = nil)
4444
options = opts || {}
4545
Validators::Options.validate(klass, key, options)
4646
@klass = klass
47-
@key = normalize_key(key)
47+
@key = normalize_aliases!(key.dup)
4848
@fields = @key.keys
49-
@options = normalize_options(options.dup)
49+
@options = normalize_options!(options.deep_dup)
5050
end
5151

5252
# Get the index name, generated using the index key.
@@ -63,39 +63,74 @@ def name
6363

6464
private
6565

66-
# Normalize the spec, in case aliased fields are provided.
66+
# Normalize the spec in-place, in case aliased fields are provided.
6767
#
6868
# @api private
6969
#
70-
# @example Normalize the spec.
71-
# specification.normalize_key(name: 1)
70+
# @example Normalize the spec in-place.
71+
# specification.normalize_aliases!(name: 1)
7272
#
73-
# @param [ Hash ] key The index key(s).
73+
# @param [ Hash ] spec The index specification.
7474
#
7575
# @return [ Hash ] The normalized specification.
76-
def normalize_key(key)
77-
normalized = {}
78-
key.each_pair do |name, direction|
79-
normalized[klass.database_field_name(name).to_sym] = direction
76+
def normalize_aliases!(spec)
77+
return unless spec.is_a?(Hash)
78+
79+
spec.transform_keys! do |name|
80+
klass.database_field_name(name).to_sym
8081
end
81-
normalized
8282
end
8383

84-
# Normalize the index options, if any are provided.
84+
# Normalize the index options in-place. Performs deep normalization
85+
# on options which have a fields hash value.
8586
#
8687
# @api private
8788
#
88-
# @example Normalize the index options.
89-
# specification.normalize_options(unique: true)
89+
# @example Normalize the index options in-place.
90+
# specification.normalize_options!(unique: true)
9091
#
91-
# @param [ Hash ] opts The index options.
92+
# @param [ Hash ] options The index options.
9293
#
9394
# @return [ Hash ] The normalized options.
94-
def normalize_options(opts)
95-
options = {}
96-
opts.each_pair do |option, value|
97-
options[MAPPINGS[option] || option] = value
95+
def normalize_options!(options)
96+
97+
options.transform_keys! do |option|
98+
option = option.to_sym
99+
MAPPINGS[option] || option
100+
end
101+
102+
%i[partial_filter_expression weights wildcard_projection].each do |key|
103+
recursive_normalize_conditionals!(options[key])
98104
end
105+
106+
options
107+
end
108+
109+
# Recursively normalizes the nested elements of an options hash in-place,
110+
# to account for $and operator (and other potential $-prefixed operators
111+
# which may be supported by MongoDB in the future.)
112+
#
113+
# @api private
114+
#
115+
# @example Recursively normalize the index options in-place.
116+
# opts = { '$and' => [{ name: { '$eq' => 'Bob' } },
117+
# { age: { '$gte' => 20 } }] }
118+
# specification.recursive_normalize_conditionals!(opts)
119+
#
120+
# @param [ Hash | Array | Object ] options The index options.
121+
#
122+
# @return [ Hash | Array | Object ] The normalized options.
123+
def recursive_normalize_conditionals!(options)
124+
case options
125+
when Hash
126+
normalize_aliases!(options)
127+
options.keys.select { |key| key.to_s.start_with?('$') }.each do |key|
128+
recursive_normalize_conditionals!(options[key])
129+
end
130+
when Array
131+
options.each { |opt| recursive_normalize_conditionals!(opt) }
132+
end
133+
99134
options
100135
end
101136
end

spec/mongoid/indexable/specification_spec.rb

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,143 @@
8080

8181
describe "#initialize" do
8282

83-
let(:spec) do
84-
described_class.new(
85-
Band,
86-
{ name: 1, title: 1, years: -1 },
87-
background: true,
88-
unique: true
89-
)
83+
context "standard case" do
84+
85+
let(:spec) do
86+
described_class.new(
87+
Band,
88+
{ name: 1, title: 1, years: -1 },
89+
background: true,
90+
unique: true
91+
)
92+
end
93+
94+
it "sets the class" do
95+
expect(spec.klass).to eq(Band)
96+
end
97+
98+
it "normalizes the key" do
99+
expect(spec.key).to eq(name: 1, title: 1, y: -1)
100+
end
101+
102+
it "normalizes the options" do
103+
expect(spec.options).to eq(background: true, unique: true)
104+
end
90105
end
91106

92-
it "sets the class" do
93-
expect(spec.klass).to eq(Band)
107+
context "with aliased field options" do
108+
109+
let(:spec) do
110+
described_class.new(
111+
Band,
112+
{ name: 1, title: 1, years: -1, d: 1 },
113+
partial_filter_expression: {
114+
name: { '$exists' => true },
115+
years: { '$gt' => 5 },
116+
d: { '$eq' => false },
117+
'$and' => [
118+
{ views: { '$gt' => 100 } },
119+
{ years: { '$lte' => 50 } }
120+
]
121+
},
122+
weights: {
123+
name: 1,
124+
years: 2
125+
},
126+
wildcard_projection: {
127+
years: 1
128+
}
129+
)
130+
end
131+
132+
it "sets the class" do
133+
expect(spec.klass).to eq(Band)
134+
end
135+
136+
it "normalizes the key" do
137+
expect(spec.key).to eq(name: 1, title: 1, y: -1, deleted: 1)
138+
end
139+
140+
it "normalizes the options" do
141+
expect(spec.options).to eq(partial_filter_expression: {
142+
name: { '$exists' => true },
143+
y: { '$gt' => 5 },
144+
deleted: { '$eq' => false },
145+
'$and': [
146+
{ views: { '$gt' => 100 } },
147+
{ y: { '$lte' => 50 } }
148+
]
149+
},
150+
weights: {
151+
name: 1,
152+
y: 2
153+
},
154+
wildcard_projection: {
155+
y: 1
156+
})
157+
end
94158
end
95159

96-
it "normalizes the key" do
97-
expect(spec.key).to eq(name: 1, title: 1, y: -1)
160+
context "with aliased field options nested inside $ operators" do
161+
162+
let(:spec) do
163+
described_class.new(
164+
Band,
165+
{ name: 1, title: 1, years: -1 },
166+
partial_filter_expression: {
167+
'$foo' => { years: { '$lte' => 50 } },
168+
'$bar' => [
169+
{ views: { '$gt' => 100 } },
170+
{ years: { '$lte' => 50 } }
171+
]
172+
}
173+
)
174+
end
175+
176+
it "sets the class" do
177+
expect(spec.klass).to eq(Band)
178+
end
179+
180+
it "normalizes the key" do
181+
expect(spec.key).to eq(name: 1, title: 1, y: -1)
182+
end
183+
184+
it "normalizes the options" do
185+
expect(spec.options).to eq(partial_filter_expression: {
186+
'$foo': { y: { '$lte' => 50 } },
187+
'$bar': [
188+
{ views: { '$gt' => 100 } },
189+
{ y: { '$lte' => 50 } }
190+
]
191+
})
192+
end
98193
end
99194

100-
it "normalizes the options" do
101-
expect(spec.options).to eq(background: true, unique: true)
195+
context "with aliased field options double-nested" do
196+
197+
let(:spec) do
198+
described_class.new(
199+
Band,
200+
{ name: 1, title: 1, years: -1 },
201+
partial_filter_expression: {
202+
'$foo' => { years: { years: { '$lte' => 50 } } },
203+
}
204+
)
205+
end
206+
207+
it "sets the class" do
208+
expect(spec.klass).to eq(Band)
209+
end
210+
211+
it "normalizes the key" do
212+
expect(spec.key).to eq(name: 1, title: 1, y: -1)
213+
end
214+
215+
it "normalizes the options" do
216+
expect(spec.options).to eq(partial_filter_expression: {
217+
'$foo': { y: { years: { '$lte' => 50 } } },
218+
})
219+
end
102220
end
103221
end
104222

0 commit comments

Comments
 (0)