Skip to content

Commit

Permalink
Add a new default mode for raw
Browse files Browse the repository at this point in the history
Returns the hashes exactly as fetched from the database.
  • Loading branch information
jamis committed Oct 17, 2024
1 parent df098f4 commit 1155311
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 68 deletions.
12 changes: 10 additions & 2 deletions lib/mongoid/contextual/mongo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,11 @@ def yield_document(document, &block)
doc = if document.respond_to?(:_id)
document
elsif criteria.raw_results?
demongoize_hash(klass, document)
if criteria.typecast_results?
demongoize_hash(klass, document)
else
document
end
else
Factory.from_db(klass, document, criteria)
end
Expand Down Expand Up @@ -1062,7 +1066,11 @@ def demongoize_with_field(field, value, is_translation)
# single document.
def process_raw_docs(raw_docs, limit)
docs = if criteria.raw_results?
raw_docs.map { |doc| demongoize_hash(klass, doc) }
if criteria.typecast_results?
raw_docs.map { |doc| demongoize_hash(klass, doc) }
else
raw_docs
end
else
mapped = raw_docs.map { |doc| Factory.from_db(klass, doc, criteria) }
eager_load(mapped)
Expand Down
50 changes: 37 additions & 13 deletions lib/mongoid/criteria.rb
Original file line number Diff line number Diff line change
Expand Up @@ -174,39 +174,63 @@ def embedded?

# Produce a clone of the current criteria object with it's "raw"
# setting set to the given value. A criteria set to "raw" will return
# all results as raw, demongoized hashes. When "raw" is not set, the
# criteria will return all results as instantiated Document instances.
# all results as raw hashes. If `typed` is true, the values in the hashes
# will be typecast according to the fields that they correspond to.
#
# When "raw" is not set (or if `raw_results` is false), the criteria will
# return all results as instantiated Document instances.
#
# @example Return query results as raw hashes:
# Person.where(city: 'Boston').raw
#
# @param [ true | false ] raw_results Whether the new criteria should be
# placed in "raw" mode or not.
# @param [ true | false ] typed Whether the raw results should be typecast
# before being returned. Default is true if raw_results is false, and
# false otherwise.
#
# @return [ Criteria ] the cloned criteria object.
def raw(raw_results = true)
def raw(raw_results = true, typed: nil)
# default for typed is true when raw_results is false, and false when
# raw_results is true.
typed = !raw_results if typed.nil?

if !typed && !raw_results
raise ArgumentError, 'instantiated results must be typecast'
end

clone.tap do |criteria|
criteria._raw_results = raw_results
criteria._raw_results = { raw: raw_results, typed: typed }
end
end

# An internal helper for setting the "raw" flag on a given criteria
# An internal helper for getting/setting the "raw" flag on a given criteria
# object.
#
# @param [ true | false ] raw_results Whether the criteria should be placed
# in "raw" mode or not.
# @return [ nil | Hash ] If set, it is a hash with two keys, :raw and :typed,
# that describe whether raw results should be returned, and whether they
# ought to be typecast.
#
# @api private
def _raw_results=(raw_results)
@raw_results = raw_results
end
attr_accessor :_raw_results

# Predicate that answers the question: is this criteria object currently
# in raw mode? (See #raw for a description of raw mode.)
#
# @return [ true | false ] whether the criteria is in raw mode or not.
def raw_results?
@raw_results
_raw_results && _raw_results[:raw]
end

# Predicate that answers the question: should the results returned by
# this criteria object be typecast? (See #raw for a description of this.)
# The answer is meaningless unless #raw_results? is true, since if
# instantiated document objects are returned they will always be typecast.
#
# @return [ true | false ] whether the criteria should return typecast
# results.
def typecast_results?
_raw_results && _raw_results[:typed]
end

# Extract a single id from the provided criteria. Could be in an $and
Expand Down Expand Up @@ -315,7 +339,7 @@ def merge!(other)
self.documents = other.documents.dup unless other.documents.empty?
self.scoping_options = other.scoping_options
self.inclusions = (inclusions + other.inclusions).uniq
self._raw_results = self.raw_results? || other.raw_results?
self._raw_results = self._raw_results || other._raw_results
self
end

Expand Down Expand Up @@ -551,7 +575,7 @@ def initialize_copy(other)
@inclusions = other.inclusions.dup
@scoping_options = other.scoping_options
@documents = other.documents.dup
@raw_results = other.raw_results?
self._raw_results = other._raw_results
@context = nil
super
end
Expand Down
197 changes: 144 additions & 53 deletions spec/mongoid/criteria_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2270,80 +2270,171 @@ def self.ages; self; end
end

describe '#raw' do
let(:results) { criteria.to_a }
let(:result) { results[0] }

context 'without associations' do
before do
Band.create(name: 'the band',
active: true,
genres: %w[ abc def ],
member_count: 112,
rating: 4.2,
created: Time.now,
updated: Time.now,
sales: 1_234_567.89,
decimal: 9_876_543.21,
decibels: 140..170,
deleted: false,
mojo: Math::PI,
tags: { 'one' => 1, 'two' => 2 },
location: LatLng.new(41.74, -111.83))
context 'when the parameters are inconsistent' do
let(:results) { criteria.raw(false, typed: false).to_a }
let(:criteria) { Person }

it 'raises an ArgumentError' do
expect { result }.to raise_error(ArgumentError)
end
end

let(:criteria) { Band.where(name: 'the band').raw }
context 'when returning untyped results' do
let(:results) { criteria.raw.to_a }

it 'returns a hash' do
expect(result).to be_a(Hash)
end
context 'without associations' do
before do
Band.create(name: 'the band',
active: true,
genres: %w[ abc def ],
member_count: 112,
rating: 4.2,
created: Time.now,
updated: Time.now,
sales: 1_234_567.89,
decimal: 9_876_543.21,
decibels: 140..170,
deleted: false,
mojo: Math::PI,
tags: { 'one' => 1, 'two' => 2 },
location: LatLng.new(41.74, -111.83))
end

let(:criteria) { Band.where(name: 'the band') }

it 'returns a hash' do
expect(result).to be_a(Hash)
end

it 'demongoizes the result' do
expect(result['genres']).to be_a(Array)
expect(result['decibels']).to be_a(Range)
expect(result['location']).to be_a(LatLng)
it 'does not demongoize the result' do
expect(result['genres']).to be_a(Array)
expect(result['decibels']).to be == { 'min' => 140, 'max' => 170 }
expect(result['location']).to be == [ -111.83, 41.74 ]
end
end
end

context 'with associations' do
before do
Person.create({
addresses: [ Address.new(end_date: 2.months.from_now) ],
passport: Passport.new(exp: 1.year.from_now)
})
context 'with associations' do
before do
Person.create({
addresses: [ Address.new(end_date: 2.months.from_now) ],
passport: Passport.new(exp: 1.year.from_now)
})
end

let(:criteria) { Person }

it 'demongoizes the embedded relation' do
expect(result['addresses']).to be_a(Array)
expect(result['addresses'][0]['end_date']).to be_a(Time)

# `pass` is how it is stored, `passport` is how it is aliased
expect(result['pass']).to be_a(Hash)
expect(result['pass']['exp']).to be_a(Time)
end
end

let(:criteria) { Person.raw }
context 'with projections' do
before { Person.create(title: 'sir', dob: Date.new(1980, 1, 1)) }

it 'demongoizes the embedded relation' do
expect(result['addresses']).to be_a(Array)
expect(result['addresses'][0]['end_date']).to be_a(Date)
context 'using #only' do
let(:criteria) { Person.only(:dob) }

# `pass` is how it is stored, `passport` is how it is aliased
expect(result['pass']).to be_a(Hash)
expect(result['pass']['exp']).to be_a(Date)
it 'produces a hash with only the _id and the requested key' do
expect(result).to be_a(Hash)
expect(result.keys).to be == %w[ _id dob ]
expect(result['dob']).to be == Date.new(1980, 1, 1)
end
end

context 'using #without' do
let(:criteria) { Person.without(:dob) }

it 'produces a hash that excludes requested key' do
expect(result).to be_a(Hash)
expect(result.keys).not_to include('dob')
expect(result.keys).to be_present
end
end
end
end

context 'with projections' do
before { Person.create(title: 'sir', dob: Date.new(1980, 1, 1)) }

context 'using #only' do
let(:criteria) { Person.only(:dob).raw }
context 'when returning typed results' do
let(:results) { criteria.raw(typed: true).to_a }

it 'produces a hash with only the _id and the requested key' do
context 'without associations' do
before do
Band.create(name: 'the band',
active: true,
genres: %w[ abc def ],
member_count: 112,
rating: 4.2,
created: Time.now,
updated: Time.now,
sales: 1_234_567.89,
decimal: 9_876_543.21,
decibels: 140..170,
deleted: false,
mojo: Math::PI,
tags: { 'one' => 1, 'two' => 2 },
location: LatLng.new(41.74, -111.83))
end

let(:criteria) { Band.where(name: 'the band') }

it 'returns a hash' do
expect(result).to be_a(Hash)
expect(result.keys).to be == %w[ _id dob ]
expect(result['dob']).to be == Date.new(1980, 1, 1)
end

it 'demongoizes the result' do
expect(result['genres']).to be_a(Array)
expect(result['decibels']).to be_a(Range)
expect(result['location']).to be_a(LatLng)
end
end

context 'using #without' do
let(:criteria) { Person.without(:dob).raw }
context 'with associations' do
before do
Person.create({
addresses: [ Address.new(end_date: 2.months.from_now) ],
passport: Passport.new(exp: 1.year.from_now)
})
end

it 'produces a hash that excludes requested key' do
expect(result).to be_a(Hash)
expect(result.keys).not_to include('dob')
expect(result.keys).to be_present
let(:criteria) { Person }

it 'demongoizes the embedded relation' do
expect(result['addresses']).to be_a(Array)
expect(result['addresses'][0]['end_date']).to be_a(Date)

# `pass` is how it is stored, `passport` is how it is aliased
expect(result['pass']).to be_a(Hash)
expect(result['pass']['exp']).to be_a(Date)
end
end

context 'with projections' do
before { Person.create(title: 'sir', dob: Date.new(1980, 1, 1)) }

context 'using #only' do
let(:criteria) { Person.only(:dob) }

it 'produces a hash with only the _id and the requested key' do
expect(result).to be_a(Hash)
expect(result.keys).to be == %w[ _id dob ]
expect(result['dob']).to be == Date.new(1980, 1, 1)
end
end

context 'using #without' do
let(:criteria) { Person.without(:dob) }

it 'produces a hash that excludes requested key' do
expect(result).to be_a(Hash)
expect(result.keys).not_to include('dob')
expect(result.keys).to be_present
end
end
end
end
Expand Down

0 comments on commit 1155311

Please sign in to comment.