Skip to content

Commit

Permalink
RUBY-2993 allow Mongoid with BSON 5 to have literal BSON::Decimal128 …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
jamis committed Jul 12, 2023
1 parent 0d31b3c commit bf44047
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 25 deletions.
37 changes: 37 additions & 0 deletions docs/release-notes/mongoid-9.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,43 @@ of the ``index`` macro: ``partial_filter_expression``, ``weights``,
and Mongoid's functionality may not support such changes.


BSON 5 and BSON::Decimal128 Fields
----------------------------------

When BSON 4 or earlier is present, any field declared as BSON::Decimal128 will
return a BSON::Decimal128 value. When BSON 5 is present, however, any field
declared as BSON::Decimal128 will return a BigDecimal value by default.

.. code-block:: ruby

class Model
include Mongoid::Document

field :decimal_field, type: BSON::Decimal128
end

# under BSON <= 4
Model.first.decimal_field.class #=> BSON::Decimal128

# under BSON >= 5
Model.first.decimal_field.class #=> BigDecimal

If you need literal BSON::Decimal128 values with BSON 5, you may instruct
Mongoid to allow literal BSON::Decimal128 fields:

.. code-block:: ruby

Model.first.decimal_field.class #=> BigDecimal

Mongoid.allow_bson5_decimal128 = true
Model.first.decimal_field.class #=> BSON::Decimal128

.. note::

The ``allow_bson5_decimal128`` option only has any effect under
BSON 5 and later. BSON 4 and earlier ignore the setting entirely.


Bug Fixes and Improvements
--------------------------

Expand Down
17 changes: 17 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ module Config
# Store BigDecimals as Decimal128s instead of strings in the db.
option :map_big_decimal_to_decimal128, default: true

# Allow BSON::Decimal128 to be parsed and returned directly in
# field values. When BSON 5 is present and the this option is set to false
# (the default), BSON::Decimal128 values in the database will be returned
# as BigDecimal.
#
# @note this option only has effect when BSON 5+ is present. Otherwise,
# the setting is ignored.
option :allow_bson5_decimal128, default: false, on_change: -> (allow) do
if BSON::VERSION >= '5.0.0'
if allow
BSON::Registry.register(BSON::Decimal128::BSON_TYPE, BSON::Decimal128)
else
BSON::Registry.register(BSON::Decimal128::BSON_TYPE, BigDecimal)
end
end
end

# Sets the async_query_executor for the application. By default the thread pool executor
# is set to `:immediate. Options are:
#
Expand Down
3 changes: 2 additions & 1 deletion lib/mongoid/config/introspection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def unindent(text)
((?:^\s*\#.*\n)+) # match one or more lines of comments
^\s+option\s+ # followed immediately by a line declaring an option
:(\w+),\s+ # match the option's name, followed by a comma
default:\s+(.*) # match the default value for the option
default:\s+(.*?) # match the default value for the option
(?:,.*?)? # skip any other configuration
\n) # end with a newline
}x

Expand Down
3 changes: 3 additions & 0 deletions lib/mongoid/config/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def defaults
# @param [ Hash ] options Extras for the option.
#
# @option options [ Object ] :default The default value.
# @option options [ Proc | nil ] :on_change The callback to invoke when the
# setter is invoked.
def option(name, options = {})
defaults[name] = settings[name] = options[:default]

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

define_method("#{name}=") do |value|
settings[name] = value
options[:on_change]&.call(value)
end

define_method("#{name}?") do
Expand Down
37 changes: 24 additions & 13 deletions lib/mongoid/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -814,21 +814,19 @@ def field_for(name, options)
#
# @api private
def retrieve_and_validate_type(name, type)
type_mapping = TYPE_MAPPINGS[type]
result = type_mapping || unmapped_type(type)
if !result.is_a?(Class)
raise Errors::InvalidFieldType.new(self, name, type)
else
if INVALID_BSON_CLASSES.include?(result)
warn_message = "Using #{result} as the field type is not supported. "
if result == BSON::Decimal128
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+."
else
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."
end
Mongoid.logger.warn(warn_message)
result = TYPE_MAPPINGS[type] || unmapped_type(type)
raise Errors::InvalidFieldType.new(self, name, type) if !result.is_a?(Class)

if unsupported_type?(result)
warn_message = "Using #{result} as the field type is not supported. "
if result == BSON::Decimal128
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.'
else
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.'
end
Mongoid.logger.warn(warn_message)
end

result
end

Expand All @@ -847,6 +845,19 @@ def unmapped_type(type)
type || Object
end
end

# Queries whether or not the given type is permitted as a declared field
# type.
#
# @param [ Class ] type The type to query
#
# @return [ true | false ] whether or not the type is supported
#
# @api private
def unsupported_type?(type)
return !Mongoid::Config.allow_bson5_decimal128? if type == BSON::Decimal128
INVALID_BSON_CLASSES.include?(type)
end
end
end
end
9 changes: 9 additions & 0 deletions spec/mongoid/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,15 @@
it_behaves_like "a config option"
end

context 'when setting the allow_bson5_decimal128 option in the config' do
min_bson_version '5.0'

let(:option) { :allow_bson5_decimal128 }
let(:default) { false }

it_behaves_like "a config option"
end

context 'when setting the legacy_readonly option in the config' do
let(:option) { :legacy_readonly }
let(:default) { false }
Expand Down
38 changes: 27 additions & 11 deletions spec/mongoid/contextual/mongo_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1092,33 +1092,49 @@
let!(:person2) { Person.create!(ssn: BSON::Decimal128.new("1")) }
let(:tally) { Person.tally("ssn") }

let(:tallied_classes) do
tally.keys.map(&:class).sort do |a, b|
a.to_s.casecmp(b.to_s)
end
end

context "< BSON 5" do
max_bson_version '4.99.99'

it "stores the correct types in the database" do
Person.find(person1.id).attributes["ssn"].should be_a BSON::Regexp::Raw
Person.find(person2.id).attributes["ssn"].should be_a BSON::Decimal128
expect(Person.find(person1.id).attributes["ssn"]).to be_a BSON::Regexp::Raw
expect(Person.find(person2.id).attributes["ssn"]).to be_a BSON::Decimal128
end

it "tallies the correct type" do
expect(tallied_classes).to be == [ BSON::Decimal128, BSON::Regexp::Raw ]
end
end

context '>= BSON 5' do
min_bson_version "5.0"

it "stores the correct types in the database" do
expect(Person.find(person1.id).ssn).to be_a BSON::Regexp::Raw
expect(Person.find(person2.id).ssn).to be_a BigDecimal
end

it "tallies the correct type" do
tally.keys.map(&:class).sort do |a,b|
a.to_s <=> b.to_s
end.should == [BSON::Decimal128, BSON::Regexp::Raw]
expect(tallied_classes).to be == [ BigDecimal, BSON::Regexp::Raw ]
end
end

context ">= BSON 5" do
context '>= BSON 5 with decimal128 allowed' do
min_bson_version "5.0"
config_override :allow_bson5_decimal128, true

it "stores the correct types in the database" do
Person.find(person1.id).ssn.should be_a BSON::Regexp::Raw
Person.find(person2.id).ssn.should be_a BigDeimal
expect(Person.find(person1.id).ssn).to be_a BSON::Regexp::Raw
expect(Person.find(person2.id).ssn).to be_a BSON::Decimal128
end

it "tallies the correct type" do
tally.keys.map(&:class).sort do |a,b|
a.to_s <=> b.to_s
end.should == [BigDecimal, BSON::Regexp::Raw]
expect(tallied_classes).to be == [ BSON::Decimal128, BSON::Regexp::Raw ]
end
end
end
Expand Down
43 changes: 43 additions & 0 deletions spec/mongoid/fields_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,49 @@
end
end
end

context 'when the field is declared as BSON::Decimal128' do
let(:document) { Mop.create!(decimal128_field: BSON::Decimal128.new(Math::PI.to_s)).reload }

shared_context 'BSON::Decimal128 is BigDecimal' do
it 'should return a BigDecimal' do
expect(document.decimal128_field).to be_a BigDecimal
end
end

shared_context 'BSON::Decimal128 is BSON::Decimal128' do
it 'should return a BSON::Decimal128' do
expect(document.decimal128_field).to be_a BSON::Decimal128
end
end

it 'is declared as BSON::Decimal128' do
expect(Mop.fields['decimal128_field'].type).to be == BSON::Decimal128
end

context 'when BSON version <= 4' do
max_bson_version '4.99.99'
it_behaves_like 'BSON::Decimal128 is BSON::Decimal128'
end

context 'when BSON version >= 5' do
min_bson_version '5.0.0'

context 'when allow_bson5_decimal128 is false' do
config_override :allow_bson5_decimal128, false
it_behaves_like 'BSON::Decimal128 is BigDecimal'
end

context 'when allow_bson5_decimal128 is true' do
config_override :allow_bson5_decimal128, true
it_behaves_like 'BSON::Decimal128 is BSON::Decimal128'
end

context 'when allow_bson5_decimal128 is default' do
it_behaves_like 'BSON::Decimal128 is BigDecimal'
end
end
end
end

describe "#getter_before_type_cast" do
Expand Down

0 comments on commit bf44047

Please sign in to comment.