Skip to content

Commit bf44047

Browse files
authored
RUBY-2993 allow Mongoid with BSON 5 to have literal BSON::Decimal128 fields (#5661)
* RUBY-2993 allow Mongoid with BSON 5 to have literal BSON::Decimal128 fields * doc formatting gets me, every time :/ * need to teach config/introspection about new option configurations * make the option name specific to bson5
1 parent 0d31b3c commit bf44047

File tree

8 files changed

+162
-25
lines changed

8 files changed

+162
-25
lines changed

docs/release-notes/mongoid-9.0.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,43 @@ of the ``index`` macro: ``partial_filter_expression``, ``weights``,
295295
and Mongoid's functionality may not support such changes.
296296

297297

298+
BSON 5 and BSON::Decimal128 Fields
299+
----------------------------------
300+
301+
When BSON 4 or earlier is present, any field declared as BSON::Decimal128 will
302+
return a BSON::Decimal128 value. When BSON 5 is present, however, any field
303+
declared as BSON::Decimal128 will return a BigDecimal value by default.
304+
305+
.. code-block:: ruby
306+
307+
class Model
308+
include Mongoid::Document
309+
310+
field :decimal_field, type: BSON::Decimal128
311+
end
312+
313+
# under BSON <= 4
314+
Model.first.decimal_field.class #=> BSON::Decimal128
315+
316+
# under BSON >= 5
317+
Model.first.decimal_field.class #=> BigDecimal
318+
319+
If you need literal BSON::Decimal128 values with BSON 5, you may instruct
320+
Mongoid to allow literal BSON::Decimal128 fields:
321+
322+
.. code-block:: ruby
323+
324+
Model.first.decimal_field.class #=> BigDecimal
325+
326+
Mongoid.allow_bson5_decimal128 = true
327+
Model.first.decimal_field.class #=> BSON::Decimal128
328+
329+
.. note::
330+
331+
The ``allow_bson5_decimal128`` option only has any effect under
332+
BSON 5 and later. BSON 4 and earlier ignore the setting entirely.
333+
334+
298335
Bug Fixes and Improvements
299336
--------------------------
300337

lib/mongoid/config.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ module Config
8080
# Store BigDecimals as Decimal128s instead of strings in the db.
8181
option :map_big_decimal_to_decimal128, default: true
8282

83+
# Allow BSON::Decimal128 to be parsed and returned directly in
84+
# field values. When BSON 5 is present and the this option is set to false
85+
# (the default), BSON::Decimal128 values in the database will be returned
86+
# as BigDecimal.
87+
#
88+
# @note this option only has effect when BSON 5+ is present. Otherwise,
89+
# the setting is ignored.
90+
option :allow_bson5_decimal128, default: false, on_change: -> (allow) do
91+
if BSON::VERSION >= '5.0.0'
92+
if allow
93+
BSON::Registry.register(BSON::Decimal128::BSON_TYPE, BSON::Decimal128)
94+
else
95+
BSON::Registry.register(BSON::Decimal128::BSON_TYPE, BigDecimal)
96+
end
97+
end
98+
end
99+
83100
# Sets the async_query_executor for the application. By default the thread pool executor
84101
# is set to `:immediate. Options are:
85102
#

lib/mongoid/config/introspection.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ def unindent(text)
122122
((?:^\s*\#.*\n)+) # match one or more lines of comments
123123
^\s+option\s+ # followed immediately by a line declaring an option
124124
:(\w+),\s+ # match the option's name, followed by a comma
125-
default:\s+(.*) # match the default value for the option
125+
default:\s+(.*?) # match the default value for the option
126+
(?:,.*?)? # skip any other configuration
126127
\n) # end with a newline
127128
}x
128129

lib/mongoid/config/options.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def defaults
2626
# @param [ Hash ] options Extras for the option.
2727
#
2828
# @option options [ Object ] :default The default value.
29+
# @option options [ Proc | nil ] :on_change The callback to invoke when the
30+
# setter is invoked.
2931
def option(name, options = {})
3032
defaults[name] = settings[name] = options[:default]
3133

@@ -39,6 +41,7 @@ def option(name, options = {})
3941

4042
define_method("#{name}=") do |value|
4143
settings[name] = value
44+
options[:on_change]&.call(value)
4245
end
4346

4447
define_method("#{name}?") do

lib/mongoid/fields.rb

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -814,21 +814,19 @@ def field_for(name, options)
814814
#
815815
# @api private
816816
def retrieve_and_validate_type(name, type)
817-
type_mapping = TYPE_MAPPINGS[type]
818-
result = type_mapping || unmapped_type(type)
819-
if !result.is_a?(Class)
820-
raise Errors::InvalidFieldType.new(self, name, type)
821-
else
822-
if INVALID_BSON_CLASSES.include?(result)
823-
warn_message = "Using #{result} as the field type is not supported. "
824-
if result == BSON::Decimal128
825-
warn_message += "In BSON <= 4, the BSON::Decimal128 type will work as expected for both storing and querying, but will return a BigDecimal on query in BSON 5+."
826-
else
827-
warn_message += "Saving values of this type to the database will work as expected, however, querying them will return a value of the native Ruby Integer type."
828-
end
829-
Mongoid.logger.warn(warn_message)
817+
result = TYPE_MAPPINGS[type] || unmapped_type(type)
818+
raise Errors::InvalidFieldType.new(self, name, type) if !result.is_a?(Class)
819+
820+
if unsupported_type?(result)
821+
warn_message = "Using #{result} as the field type is not supported. "
822+
if result == BSON::Decimal128
823+
warn_message += 'In BSON <= 4, the BSON::Decimal128 type will work as expected for both storing and querying, but will return a BigDecimal on query in BSON 5+. To use literal BSON::Decimal128 fields with BSON 5, set Mongoid.allow_bson5_decimal128 to true.'
824+
else
825+
warn_message += 'Saving values of this type to the database will work as expected, however, querying them will return a value of the native Ruby Integer type.'
830826
end
827+
Mongoid.logger.warn(warn_message)
831828
end
829+
832830
result
833831
end
834832

@@ -847,6 +845,19 @@ def unmapped_type(type)
847845
type || Object
848846
end
849847
end
848+
849+
# Queries whether or not the given type is permitted as a declared field
850+
# type.
851+
#
852+
# @param [ Class ] type The type to query
853+
#
854+
# @return [ true | false ] whether or not the type is supported
855+
#
856+
# @api private
857+
def unsupported_type?(type)
858+
return !Mongoid::Config.allow_bson5_decimal128? if type == BSON::Decimal128
859+
INVALID_BSON_CLASSES.include?(type)
860+
end
850861
end
851862
end
852863
end

spec/mongoid/config_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,15 @@
336336
it_behaves_like "a config option"
337337
end
338338

339+
context 'when setting the allow_bson5_decimal128 option in the config' do
340+
min_bson_version '5.0'
341+
342+
let(:option) { :allow_bson5_decimal128 }
343+
let(:default) { false }
344+
345+
it_behaves_like "a config option"
346+
end
347+
339348
context 'when setting the legacy_readonly option in the config' do
340349
let(:option) { :legacy_readonly }
341350
let(:default) { false }

spec/mongoid/contextual/mongo_spec.rb

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,33 +1092,49 @@
10921092
let!(:person2) { Person.create!(ssn: BSON::Decimal128.new("1")) }
10931093
let(:tally) { Person.tally("ssn") }
10941094

1095+
let(:tallied_classes) do
1096+
tally.keys.map(&:class).sort do |a, b|
1097+
a.to_s.casecmp(b.to_s)
1098+
end
1099+
end
1100+
10951101
context "< BSON 5" do
10961102
max_bson_version '4.99.99'
10971103

10981104
it "stores the correct types in the database" do
1099-
Person.find(person1.id).attributes["ssn"].should be_a BSON::Regexp::Raw
1100-
Person.find(person2.id).attributes["ssn"].should be_a BSON::Decimal128
1105+
expect(Person.find(person1.id).attributes["ssn"]).to be_a BSON::Regexp::Raw
1106+
expect(Person.find(person2.id).attributes["ssn"]).to be_a BSON::Decimal128
1107+
end
1108+
1109+
it "tallies the correct type" do
1110+
expect(tallied_classes).to be == [ BSON::Decimal128, BSON::Regexp::Raw ]
1111+
end
1112+
end
1113+
1114+
context '>= BSON 5' do
1115+
min_bson_version "5.0"
1116+
1117+
it "stores the correct types in the database" do
1118+
expect(Person.find(person1.id).ssn).to be_a BSON::Regexp::Raw
1119+
expect(Person.find(person2.id).ssn).to be_a BigDecimal
11011120
end
11021121

11031122
it "tallies the correct type" do
1104-
tally.keys.map(&:class).sort do |a,b|
1105-
a.to_s <=> b.to_s
1106-
end.should == [BSON::Decimal128, BSON::Regexp::Raw]
1123+
expect(tallied_classes).to be == [ BigDecimal, BSON::Regexp::Raw ]
11071124
end
11081125
end
11091126

1110-
context ">= BSON 5" do
1127+
context '>= BSON 5 with decimal128 allowed' do
11111128
min_bson_version "5.0"
1129+
config_override :allow_bson5_decimal128, true
11121130

11131131
it "stores the correct types in the database" do
1114-
Person.find(person1.id).ssn.should be_a BSON::Regexp::Raw
1115-
Person.find(person2.id).ssn.should be_a BigDeimal
1132+
expect(Person.find(person1.id).ssn).to be_a BSON::Regexp::Raw
1133+
expect(Person.find(person2.id).ssn).to be_a BSON::Decimal128
11161134
end
11171135

11181136
it "tallies the correct type" do
1119-
tally.keys.map(&:class).sort do |a,b|
1120-
a.to_s <=> b.to_s
1121-
end.should == [BigDecimal, BSON::Regexp::Raw]
1137+
expect(tallied_classes).to be == [ BSON::Decimal128, BSON::Regexp::Raw ]
11221138
end
11231139
end
11241140
end

spec/mongoid/fields_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,49 @@
559559
end
560560
end
561561
end
562+
563+
context 'when the field is declared as BSON::Decimal128' do
564+
let(:document) { Mop.create!(decimal128_field: BSON::Decimal128.new(Math::PI.to_s)).reload }
565+
566+
shared_context 'BSON::Decimal128 is BigDecimal' do
567+
it 'should return a BigDecimal' do
568+
expect(document.decimal128_field).to be_a BigDecimal
569+
end
570+
end
571+
572+
shared_context 'BSON::Decimal128 is BSON::Decimal128' do
573+
it 'should return a BSON::Decimal128' do
574+
expect(document.decimal128_field).to be_a BSON::Decimal128
575+
end
576+
end
577+
578+
it 'is declared as BSON::Decimal128' do
579+
expect(Mop.fields['decimal128_field'].type).to be == BSON::Decimal128
580+
end
581+
582+
context 'when BSON version <= 4' do
583+
max_bson_version '4.99.99'
584+
it_behaves_like 'BSON::Decimal128 is BSON::Decimal128'
585+
end
586+
587+
context 'when BSON version >= 5' do
588+
min_bson_version '5.0.0'
589+
590+
context 'when allow_bson5_decimal128 is false' do
591+
config_override :allow_bson5_decimal128, false
592+
it_behaves_like 'BSON::Decimal128 is BigDecimal'
593+
end
594+
595+
context 'when allow_bson5_decimal128 is true' do
596+
config_override :allow_bson5_decimal128, true
597+
it_behaves_like 'BSON::Decimal128 is BSON::Decimal128'
598+
end
599+
600+
context 'when allow_bson5_decimal128 is default' do
601+
it_behaves_like 'BSON::Decimal128 is BigDecimal'
602+
end
603+
end
604+
end
562605
end
563606

564607
describe "#getter_before_type_cast" do

0 commit comments

Comments
 (0)