Compare activerecord results with sorted results - ruby-on-rails

I am writing a test in rspec to make sure a function is returning a correctly sorted result set. The test looks like this:
it 'returns matching accounts' do
results = Account.search(<search conditions>)
expect(results).to eq results.order(<order clause>)
end
The result sets appear to be equivalent, but the test still fails. The following test does pass:
it 'returns matching accounts' do
results = Account.search(<search conditions>)
expect(results.map &:id).to eq results.order(<order clause>).map &:id
end
I am satisfied that this test does what I need, but is there a better way to compare two ActiveRecord relations?

Account.search probably doesn't return an array of accounts, but a ActiveRecord::Relation. When you call order on a relation it will return a different relation object and therefore not match.
On the other hand if you call an method on that relation that forces the relation to actually run the query against the database, then an array is returned. map is such a method, other might be each, first, any?, load, etc.
load seems to be a good choice to use, to force actually loading the records from the database:
it 'returns matching accounts' do
results = Account.search(<search conditions>)
expect(results.load).to eq results.order(<order clause>).load
end
Another option might be to compare the result against a array, that was sorted in Ruby instead of in the database. It depends on what you really want to test, if this is a better option:
it 'returns matching accounts' do
results = Account.search(<search conditions>).load
expect(results).to eq results.sort_by(&:attribute_to_sort_on)
end

Related

Can I force the execution of an active record query chain?

I have an edge case where I want to use .first only after my SQL query has been executed.
My case is the next one:
User.select("sum((type = 'foo')::int) as foo_count",
"sum((type = 'bar')::int) as bar_count")
.first
.yield_self { |r| r.bar_count / r.foo_count.to_f }
However, this would throw an SQL error saying that I should include my user_id in the GROUP BY clause. I've already found a hacky solution using to_a, but I really wonder if there is a proper way to force execution before my call to .first.
The error is because first uses an order by statement to order by id.
"Find the first record (or first N records if a parameter is supplied). If no order is defined it will order by primary key."
Instead try take
"Gives a record (or N records if a parameter is supplied) without any implied order. The order will depend on the database implementation. If an order is supplied it will be respected."
So
User.select("sum((type = 'foo')::int) as foo_count",
"sum((type = 'bar')::int) as bar_count")
.take
.yield_self { |r| r.bar_count / r.foo_count.to_f }
should work appropriately however as stated the order is indeterminate.
You may want to use pluck which retrieves only the data instead of select which just alters which fields get loaded into models:
User.pluck(
"sum((type = 'foo')::int) as foo_count",
"sum((type = 'bar')::int) as bar_count"
).map do |foo_count, bar_count|
bar_count / foo_count.to_f
end
You can probably do the division in the query as well if necessary.

Select with count distinct in ActiveRecord query not returning aggregated fields

I'm doing a select with a count-distinct in ActiveRecord, but it's not returning any of my aggregated fields.
User.
select(
'users.id, count(distinct(shc.id)) as shipping_credit_count,
count(distinct(sc.id)) as service_credit_count'
).
...
...
group('users.id')
Is only returning #<ActiveRecord::Relation [#<User id: 119>]> I was expecting to see the count in my aggregated fields? Why is nothing being returned?
Your query probably works as expected but the inspect method is throwing you of. Read my answer here for a better description: Why group calculation fields do not show up in query result?
You should be able to call service_credit_count and service_credit_count on your objects even though it does not show up when you log them.
I would however implement it a little bit different. I would on the User model add the methods
def service_credit_count
return service_credit_count_sql if self.respond_to?(:service_credit_count_sql)
services.count
end
def shipping_credit_count
return shipping_credit_count_sql if self.respond_to?(:shipping_credit_count_sql)
shippings.count
end
And then in your query name the fields with the suffix. This way you can always use these counts. There is also a small (quite imature) gem I have written that does this: https://github.com/trialbee/association_count

Rails and Arel's where function: Can I call where on objects instead of making a call to the database?

Consider the following:
budget has many objects in its another_collection.
obj has many objects of the same type as object in its another_collection.
budget and some_collection are already declared before the following loop
they've been previous saved in the database and have primary keys set.
some_collection is a collections of objs.
-
some_collection.each do |obj|
another_obj = obj.another_collection.build
budget.another_collection << another_obj
end
budget.another_collection.collect {|another_obj| another_obj.another_obj_id}
=> [1, 2, 3, 4]
some_obj_with_pk_1 = some_collection.where(obj_id: obj.id)
some_obj_with_pl_1.id
=> 1
budget.another_collection.where(another_obj_id: some_obj_with_pk_1.id)
=> []
This shouldn't happen. What is happening is that rails queries the database for any items in another_collection with another_obj_id = 1. Since this collection hasn't been saved to the database yet, none of these items are showing up in the results.
Is there a function or something I can pass to Arel's where method that says to use local results only? I know I can just iterate over the items and find it but it would be great if I didn't have to do that and just use a method that does this already.
You could always use Enumeable#select which takes a block and returns only the elements that the block returns true for. You'd want to make sure that you had ActiveRecord retrieve the result set first (by calling to_a on your query).
records = Model.where(some_attribute: value).to_a
filtered_records = records.select { |record| record.something? }
Depending on your result set and your needs, it is possible that a second database query would be faster, as your SQL store is better suited to do these comparisons than Ruby. But if your records have yet to be saved, you would need to do something like the above, since the records aren't persisted yet

Can't get Array's count method to work with a block

I have a Rails app that contains a TeamSeason model class. This class has a has_one association with a Team model class, and a has_many association to another TeamSeason called opponents. I'm now trying to write a method that passes in a Team and determines whether any of its opponents are associated with that Team. The method I wrote looks something like this:
def plays?(against_team)
total = opponents.count {|opponent| opponent.team == against_team}
return (total > 0)
end
The count method should count the number of array elements that yield a true value with the block I specified. However, it appears that it is always returning the full length of the array. It's as if the block I specified always yields a true value, no matter what.
I added various puts calls to try and figure out where my logic is going wrong. Here's my observations:
When I add any puts calls inside the block next to the count method, I do not see any output for those statements. It appears that the contents of the block are never being executed
When I insert an additional loop using the opponents array's each method and a block, I can print the value of my array objects and confirm they are evaluate as I expect. I can even puts the value of opponent.team == against_team and verify that the block I wrote evaluates to false some of the time, as it should.
What am I missing here?
opponents is not a standard ruby Array - it's an ActiveRecord association proxy and behaves differently for certain methods. count will query the database for the number of opponents, and the block you are passing will not be evaluated. A simpler way to do what you want is like so:
def plays?(against_team)
opponents.joins(:team).where(teams: {id: against_team.id}).exists?
end
This will ask the database what you want, and avoid loading all opponents when you are only checking for a specific match. Alternatively, you can load the entire opponents list and use any?:
def plays?(against_team)
opponents.any?{|opponent| opponent.team == against_team.id}
end
Note that this will not only load all opponents, but it will load each opponent's team one at a time - resulting in an N+1 query (which leads to performance issues). You can avoid this by eager loading the associated team using includes():
opponents.includes(:team).any?{...}
The functionality which you are trying can be achieved using include? method.
Try:
def plays?(against_team)
opponents.collect(&:team).include?(against_team)
end

Enforcing uniqueness on a relation based on a single column

I have a class method on my Consumer model that functions as a scope (acts on an ActiveRecord::Relation, returns an ActiveRecord::Relation, can be daisy-chained) and returns doubles of some consumers. I want it to only return unique consumers, and I can't seem to find either a way to do it with Rails helpers or the right SQL to pass in to get what I want. I feel like there's a simple solution here - it's not a complicated problem.
I essentially want Array#uniq, but for ActiveRecord::Relation. I tried a select DISTINCT statement
Consumer.joins(...).where(...).select('DISTINCT consumers.id')
It returned the correct 'uniqueness on consumers.id' property, but it only returned the consumer ids, so that when I eventually loaded the relation, the Consumers didn't have their other attributes.
Without select DISTINCT:
my_consumer = Consumer.joins(...).where(...).first
my_consumer.status # => "active"
With select DISTINCT:
my_consumer = Consumer.joins(...).where(...).select('DISTINCT consumers.id').first
my_consumer.status # => ActiveModel::MissingAttributeError: missing attribute: status
I didn't think that an ActiveRecord::Relation would ever load less than the whole model, but I guess it will with the select statement. When I query the Relation after the select statement for the class it contains, it returns Consumer, which seems strange.
.select('DISTINCT consumers.*')

Resources