Skip to content

Commit

Permalink
Merge branch 'master' into custom-field-type-support
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed Jan 21, 2022
2 parents 0049e29 + 0a65cc0 commit b14e0ba
Show file tree
Hide file tree
Showing 33 changed files with 1,968 additions and 400 deletions.
18 changes: 17 additions & 1 deletion .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,10 @@ axes:
display_name: "Driver-min (JRuby)"
variables:
DRIVER: "min-jruby"
- id: bson-min
display_name: "BSON-min"
variables:
DRIVER: "bson-min"
- id: "rails"
display_name: Rails Version
values:
Expand Down Expand Up @@ -663,7 +667,19 @@ buildvariants:
run_on:
- rhel70-small
tasks:
- name: "test"
- name: "test"

- matrix_name: "bson-min"
matrix_spec:
driver: [min]
ruby: ["ruby-2.7"]
mongodb-version: "5.0"
topology: "standalone"
display_name: "${ruby}, ${driver}, ${mongodb-version}, ${topology}"
run_on:
- ubuntu1804-small
tasks:
- name: "test"

- matrix_name: "rails-6"
matrix_spec:
Expand Down
5 changes: 4 additions & 1 deletion .evergreen/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ elif test "$DRIVER" = "oldstable"; then
elif test "$DRIVER" = "min"; then
bundle install --gemfile=gemfiles/driver_min.gemfile
BUNDLE_GEMFILE=gemfiles/driver_min.gemfile
elif test "$DRIVER" = "bson-min"; then
bundle install --gemfile=gemfiles/bson_min.gemfile
BUNDLE_GEMFILE=gemfiles/bson_min.gemfile
elif test "$DRIVER" = "stable-jruby"; then
bundle install --gemfile=gemfiles/driver_stable_jruby.gemfile
BUNDLE_GEMFILE=gemfiles/driver_stable_jruby.gemfile
Expand Down Expand Up @@ -104,7 +107,7 @@ elif test -n "$APP_TESTS"; then
bash $HOME/n stable
export PATH=$HOME/.n/bin:$PATH
npm -g install yarn

bundle exec rspec spec/integration/app_spec.rb
else
bundle exec rake ci
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/configuration.txt
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ for details on driver options.
# Ruby on Rails logger instance. (default: :info)
log_level: :info

# When using the BigDecimal field type, store the value in the database
# as a BSON::Decimal128 instead of a string. (default: false)
map_big_decimal_to_decimal128: false

# Preload all models in development, needed when models use
# inheritance. (default: false)
preload_models: false
Expand Down
130 changes: 115 additions & 15 deletions docs/reference/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ You can safely omit type specifications when:

Types that are not supported as dynamic attributes since they cannot be cast are:

- ``BigDecimal``
- ``Date``
- ``DateTime``
- ``Range``
Expand Down Expand Up @@ -256,7 +255,7 @@ assignment to a ``:time`` field:

field :registered_at, type: :time
end

Voter.new(registered_at: Date.today)
# => #<Voter _id: 5fdd80392c97a618f07ba344, registered_at: 2020-12-18 05:00:00 UTC>

Expand Down Expand Up @@ -422,6 +421,107 @@ matches strings containing "hello" before a newline, besides strings ending in
This is because the meaning of ``$`` is different between PCRE and Ruby
regular expressions.

BigDecimal Fields
-----------------

The ``BigDecimal`` field type is used to store numbers with increased precision.

The ``BigDecimal`` field type stores its values in two different ways in the
database, depending on the value of the ``Mongoid.map_big_decimal_to_decimal128``
global config option. If this flag is set to false (which is the default),
the ``BigDecimal`` field will be stored as a string, otherwise it will be stored
as a ``BSON::Decimal128``.

The ``BigDecimal`` field type has some limitations when converting to and from
a ``BSON::Decimal128``:

- ``BSON::Decimal128`` has a limited range and precision, while ``BigDecimal``
has no restrictions in terms of range and precision. ``BSON::Decimal128`` has
a max value of approximately ``10^6145`` and a min value of approximately
``-10^6145``, and has a maximum of 34 bits of precision. When attempting to
store values that don't fit into a ``BSON::Decimal128``, it is recommended to
have them stored as a string instead of a ``BSON::Decimal128``. You can do
that by setting ``Mongoid.map_big_decimal_to_decimal128`` to ``false``. If a
value that does not fit in a ``BSON::Decimal128`` is attempted to be stored
as one, an error will be raised.

- ``BSON::Decimal128`` is able to accept signed ``NaN`` values, while
``BigDecimal`` is not. When retrieving signed ``NaN`` values from
the database using the ``BigDecimal`` field type, the ``NaN`` will be
unsigned.

- ``BSON::Decimal128`` maintains trailing zeroes when stored in the database.
``BigDecimal``, however, does not maintain trailing zeroes, and therefore
retrieving ``BSON::Decimal128`` values using the ``BigDecimal`` field type
may result in a loss of precision.

There is an additional caveat when storing a ``BigDecimal`` in a field with no
type (i.e. a dynamically typed field) and ``Mongoid.map_big_decimal_to_decimal128``
is ``false``. In this case, the ``BigDecimal`` is stored as a string, and since a
dynamic field is being used, querying for that field with a ``BigDecimal`` will
not find the string for that ``BigDecimal``, since the query is looking for a
``BigDecimal``. In order to query for that string, the ``BigDecimal`` must
first be converted to a string with ``to_s``. Note that this is not a problem
when the field has type ``BigDecimal``.

If you wish to avoid using ``BigDecimal`` altogether, you can set the field
type to ``BSON::Decimal128``. This will allow you to keep track of trailing
zeroes and signed ``NaN`` values.

Migration to ``decimal128``-backed ``BigDecimal`` Field
```````````````````````````````````````````````````````
In a future major version of Mongoid, the ``Mongoid.map_big_decimal_to_decimal128``
global config option will be defaulted to ``true``. When this flag is turned on,
``BigDecimal`` values in queries will not match to the strings that are already
stored in the database; they will only match to ``decimal128`` values that are
in the database. If you have a ``BigDecimal`` field that is backed by strings,
you have three options:

1. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
set to ``false``, and you can continue storing your ``BigDecimal`` values as
strings. Note that you are surrendering the advantages of storing ``BigDecimal``
values as a ``decimal128``, like being able to do queries and aggregations
based on the numerical value of the field.

2. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
set to ``true``, and you can convert all values for that field from strings to
``decimal128`` values in the database. You should do this conversion before
setting the global config option to true. An example query to accomplish this
is as follows:

.. code-block:: javascript

db.bands.updateMany({
"field": { "$exists": true }
}, [
{
"$set": {
"field": { "$toDecimal": "$field" }
}
}
])

This query updates all documents that have the given field, setting that
field to its corresponding ``decimal128`` value. Note that this query only
works in MongoDB 4.2+.

3. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
set to ``true``, and you can have both strings and ``decimal128`` values for
that field. This way, only ``decimal128`` values will be inserted into and
updated to the database going forward. Note that you still don't get the
full advantages of using only ``decimal128`` values, but your dataset is
slowly migrating to all ``decimal128`` values, as old string values are
updated to ``decimal128`` and new ``decimal128`` values are added. With this
setup, you can still query for ``BigDecimal`` values as follows:

.. code-block:: ruby

Mongoid.map_big_decimal_to_decimal128 = true
big_decimal = BigDecimal('2E9')
Band.in(sales: [big_decimal, big_decimal.to_s]).to_a

This query will find all values that are either a ``decimal128`` value or
a string that match that value.

.. _field-default-values:

Expand Down Expand Up @@ -530,17 +630,17 @@ from the aliased field:

class Band
include Mongoid::Document

field :name, type: String
alias_attribute :n, :name
end

band = Band.new(n: 'Astral Projection')
# => #<Band _id: 5fc1c1ee2c97a64accbeb5e1, name: "Astral Projection">

band.attributes
# => {"_id"=>BSON::ObjectId('5fc1c1ee2c97a64accbeb5e1'), "name"=>"Astral Projection"}

band.n
# => "Astral Projection"

Expand All @@ -565,11 +665,11 @@ This is useful for storing different values in ``id`` and ``_id`` fields:

class Band
include Mongoid::Document

unalias_attribute :id
field :id, type: String
end

Band.new(id: '42')
# => #<Band _id: 5fc1c3f42c97a6590684046c, id: "42">

Expand Down Expand Up @@ -697,19 +797,19 @@ getter as follows:

class DistanceMeasurement
include Mongoid::Document

field :value, type: Float
field :unit, type: String

def unit
read_attribute(:unit) || "m"
end

def to_s
"#{value} #{unit}"
end
end

measurement = DistanceMeasurement.new(value: 2)
measurement.to_s
# => "2.0 m"
Expand All @@ -723,18 +823,18 @@ may be implemented as follows:

class DistanceMeasurement
include Mongoid::Document

field :value, type: Float
field :unit, type: String

def unit=(value)
if value.blank?
value = nil
end
write_attribute(:unit, value)
end
end

measurement = DistanceMeasurement.new(value: 2, unit: "")
measurement.attributes
# => {"_id"=>BSON::ObjectId('613fa15aa15d5d617216104c'), "value"=>2.0, "unit"=>nil}
Expand Down
11 changes: 11 additions & 0 deletions gemfiles/bson_min.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source "https://rubygems.org"
gemspec path: '..'

gem 'bson', '4.14.0'
# This configuration doesn't require a specific driver version. When bson-4.14.0
# was released, current driver version was 2.17.0
gem 'mongo', '2.17.0'

require_relative './standard'

standard_dependencies
1 change: 1 addition & 0 deletions lib/mongoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require 'mongo/active_support'

require "mongoid/version"
require "mongoid/deprecable"
require "mongoid/config"
require "mongoid/persistence_context"
require "mongoid/loggable"
Expand Down
3 changes: 3 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ module Config
# Return stored times as UTC.
option :use_utc, default: false

# Store BigDecimals as Decimal128s instead of strings in the db.
option :map_big_decimal_to_decimal128, default: false

# Has Mongoid been configured? This is checking that at least a valid
# client config exists.
#
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/contextual/mongo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class Mongo
include Association::EagerLoadable
include Queryable

Mongoid.deprecate(self, :geo_near)

# Options constant.
OPTIONS = [ :hint,
:limit,
Expand Down
10 changes: 9 additions & 1 deletion lib/mongoid/criteria/queryable/extensions/big_decimal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ module ClassMethods
# @return [ String ] The big decimal as a string.
def evolve(object)
__evolve__(object) do |obj|
obj ? obj.to_s : obj
if obj
if obj.is_a?(::BigDecimal) && Mongoid.map_big_decimal_to_decimal128
BSON::Decimal128.new(obj)
elsif obj.is_a?(BSON::Decimal128)
obj
else
obj.to_s
end
end
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/criteria/queryable/selectable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Queryable
module Selectable
extend Macroable

Mongoid.deprecate(self, :geo_spacial)

# Constant for a LineString $geometry.
LINE_STRING = "LineString"

Expand Down
36 changes: 36 additions & 0 deletions lib/mongoid/deprecable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "mongoid/deprecation"

module Mongoid

# Adds ability to declare Mongoid-specific deprecations.
#
# @api private
module Deprecable

# Declares method(s) as deprecated.
#
# @example Deprecate a method.
# Mongoid.deprecate(Cat, :meow); Cat.new.meow
# #=> Mongoid.logger.warn("meow is deprecated and will be removed from Mongoid 8.0")
#
# @example Deprecate a method and declare the replacement method.
# Mongoid.deprecate(Cat, meow: :speak); Cat.new.meow
# #=> Mongoid.logger.warn("meow is deprecated and will be removed from Mongoid 8.0 (use speak instead)")
#
# @example Deprecate a method and give replacement instructions.
# Mongoid.deprecate(Cat, meow: 'eat :catnip instead'); Cat.new.meow
# #=> Mongoid.logger.warn("meow is deprecated and will be removed from Mongoid 8.0 (eat :catnip instead)")
#
# @param [ Module ] target_module The parent which contains the method.
# @param [ Symbol | Hash<Symbol, [Symbol|String]> ] method_descriptors
# The methods to deprecate, with optional replacement instructions.
def deprecate(target_module, *method_descriptors)
Mongoid::Deprecation.deprecate_methods(target_module, *method_descriptors)
end
end
end

# Ensure Mongoid.deprecate can be used during initialization
Mongoid.extend(Mongoid::Deprecable)
25 changes: 25 additions & 0 deletions lib/mongoid/deprecation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Mongoid

# Utility class for logging deprecation warnings.
class Deprecation < ::ActiveSupport::Deprecation

@gem_name = 'Mongoid'

# Per change policy, deprecations will be removed in the next major version.
@deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0".freeze

# Overrides default ActiveSupport::Deprecation behavior
# to use Mongoid's logger.
#
# @return Array<Proc> The deprecation behavior.
def behavior
@behavior ||= Array(->(message, callstack, _deprecation_horizon, _gem_name) {
logger = Mongoid.logger
logger.warn(message)
logger.debug(callstack.join("\n ")) if debug
})
end
end
end
Loading

0 comments on commit b14e0ba

Please sign in to comment.