Modify Ransack not_in query to include when field is nil - sql

I am querying my model like so:
MyModel.search(year_not_in: [2000, 2010, 2015])
But am getting back results from MyModel ONLY where year is not nil. For example, the query hits where MyModel.year = 1999 but not where MyModel.year = nil. I expect nil to not be in that array but perhaps that is how it's defined in SQL.
Is there a way I can override the year or year_not_in ransacker to add the condition above?

I was able to solve this without overriding, though I would still be interested in how to do that. Joining statements like so was my solution:
.search(m: 'or', year_not_in: [2000, 2010, 2015], year_null: true)

First of all : this solution test with ransack (2.4.2) and rails 6.1.3
To achieve this, GEM ransack give us a solution is to add a new predicate :
If you'd like to add your own custom Ransack predicates
not_in_or_null
You can do it like this way :
"config/initializers/ransack.rb"
You add this
Ransack.configure do |config|
config.add_predicate 'not_in_or_null', arel_predicate: 'not_in_or_null'
end
module Arel
module Predications
def not_in_or_null(other)
left = not_in(other)
right = eq(nil)
left.or(right)
end
end
end
And than, you have a new predicate now.

Related

Why does Postgres not accept my count column?

I am building a Rails app with the following models:
# vote.rb
class Vote < ApplicationRecord
belongs_to :person
belongs_to :show
scope :fulfilled, -> { where(fulfilled: true) }
scope :unfulfilled, -> { where(fulfilled: false) }
end
# person.rb
class Person < ApplicationRecord
has_many :votes, dependent: :destroy
def self.order_by_votes(show = nil)
count = 'nullif(votes.fulfilled, true)'
count = "case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end" if show
people = left_joins(:votes).group(:id).uniq!(:group)
people = people.select("people.*, COUNT(#{count}) AS people.vote_count")
people.order('people.vote_count DESC')
end
end
The idea behind order_by_votes is to sort People by the number of unfulfilled votes, either counting all votes, or counting only votes associated with a given Show.
This seem to work fine when I test against SQLite. But when I switch to Postgres I get this error:
Error:
PeopleControllerIndexTest#test_should_get_previously_on_show:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column people.vote_count does not exist
LINE 1: ...s"."show_id" = $1 GROUP BY "people"."id" ORDER BY people.vot...
^
If I dump the SQL using #people.to_sql, this is what I get:
SELECT people.*, COUNT(nullif(votes.fulfilled, true)) AS people.vote_count FROM "people" LEFT OUTER JOIN "votes" ON "votes"."person_id" = "people"."id" GROUP BY "people"."id" ORDER BY people.vote_count DESC
Why is this failing on Postgres but working on SQLite? And what should I be doing instead to make it work on Postgres?
(PS: I named the field people.vote_count, with a dot, so I can access it in my view without having to do another SQL query to actually view the vote count for each person in the view (not sure if this works) but I get the same error even if I name the field simply vote_count.)
(PS2: I recently added the .uniq!(:group) because of some deprecation warning for Rails 6.2, but I couldn't find any documentation for it so I am not sure I am doing it right, still the error is there without that part.)
Are you sure you're not getting a syntax error from PostgreSQL somewhere? If you do something like this:
select count(*) as t.vote_count from t ... order by t.vote_count
I get a syntax error before PostgreSQL gets to complain about there being no t.vote_count column.
No matter, the solution is to not try to put your vote_count in the people table:
people = people.select("people.*, COUNT(#{count}) AS vote_count")
...
people.order(vote_count: :desc)
You don't need it there, you'll still be able to reference the vote_count just like any "normal" column in people. Anything in the select list will appear as an accessor in the resultant model instances whether they're columns or not, they won't show up in the #inspect output (since that's generated based on the table's columns) but you call the accessor methods nonetheless.
Historically there have been quite a few AR problems (and bugs) in getting the right count by just using count on a scope, and I am not sure they are actually all gone.
That depends on the scope (AR version, relations, group, sort, uniq, etc). A defaut count call that a gem has to generically use on a scope is not a one-fit-all solution. For that known reason Pagy allows you to pass the right count to its pagy method as explained in the Pagy documentation.
Your scope might become complex and the default pagy collection.count(:all) may not get the actual count. In that case you can get the right count with some custom statement, and pass it to pagy.
#pagy, #records = pagy(collection, count: your_count)
Notice: pagy will efficiently skip its internal count query and will just use the passed :count variable.
So... just get your own calculated count and pass it to pagy, and it will not even try to use the default.
EDIT: I forgot to mention: you may want to try the pagy arel extra that:
adds specialized pagination for collections from sql databases with GROUP BY clauses, by computing the total number of results with COUNT(*) OVER ().
Thanks to all the comments and answers I have finally found a solution which I think is the best way to solve this.
First of, the issue occurred when I called pagy which tried to count my scope by appending .count(:all). This is what caused the errors. The solution was to not create a "field" in select() and use it in .order().
So here is the proper code:
def self.order_by_votes(show = nil)
count = if show
"case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end"
else
'nullif(votes.fulfilled, true)'
end
left_joins(:votes).group(:id)
.uniq!(:group)
.select("people.*, COUNT(#{count}) as vote_count")
.order(Arel.sql("COUNT(#{count}) DESC"))
end
This sorts the number of people on the number of unfulfilled votes for them, with the ability to count only votes for a given show, and it works with pagy(), and pagy_arel() which in my case is a much better fit, so the results can be properly paginated.

Rails Order by frequency of a column in another table

I have a table KmRelationship which associates Keywords and Movies
In keyword index I would like to list all keywords that appear most frequently in the KmRelationships table and only take(20)
.order doesn't seem to work no matter how I use it and where I put it and same for sort_by
It sounds relatively straight forward but i just can't seem to get it to work
Any ideas?
Assuming your KmRelationship table has keyword_id:
top_keywords = KmRelationship.select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20)
This may not look right in your console output, but that's because rails doesn't build out an object attribute for the calculated frequency column.
You can see the results like this:
top_keywords.each {|k| puts "#{k.keyword_id} : #{k.freqency}" }
To put this to good use, you can then map out your actual Keyword objects:
class Keyword < ActiveRecord::Base
# other stuff
def self.most_popular
KmRelationship.
select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20).
map(&:keyword)
end
end
And call with:
Keyword.most_popular
#posts = Post.select([:id, :title]).order("created_at desc").limit(6)
I have this listed in my controller index method which allows the the order to show the last post with a limit of 6. It might be something similar to what you are trying to do. This code actually reflects a most recent post on my home page.

Arel: Left outer join using symbols

I have this use case where I get the symbolized deep associations from a certain model, and I have to perform certain queries that involve using outer joins. How can one do it WITHOUT resorting to write the full SQL by hand?
Answers I don't want:
- using includes (doesn't solve deep associations very well ( .includes(:cars => [:windows, :engine => [:ignition]..... works unexpectedly ) and I don't want its side-effects)
- writing the SQL myself (sorry, it's 2013, cross-db support, etc etc..., and the objects I fetch are read_only, more side-effects)
I'd like to have an Arel solution. I know that using the arel_table's from the models I can construct SQL expressions, there's also a DSL for the joins, but somehow i cannot use it in the joins method from the model:
car = Car.arel_table
engine = Engine.arel_table
eng_exp = car.join(engine).on(car[:engine_id].eq(engine[:id]))
eng_exp.to_sql #=> GOOD! very nice!
Car.joins(eng_exp) #=> Breaks!!
Why this doesn't work is beyond me. I don't know exactly what is missing. But it's the closest thing to a solution I have now. If somebody could help me completing my example or provide me with a nice work-around or tell me when will Rails include such an obviously necessary feature will have my everlasting gratitude.
This is an old question, but for the benefit of anyone finding it through search engines:
If you want something you can pass into .joins, you can either use .create_join and .create_on:
join_on = car.create_on(car[:engine_id].eq(engine[:id]))
eng_join = car.create_join(engine, join_on, Arel::Nodes::OuterJoin)
Car.joins(eng_join)
OR
use the .join_sources from your constructed join object:
eng_exp = car.join(engine, Arel::Nodes::OuterJoin).on(car[:engine_id].eq(engine[:id]))
Car.joins(eng_exp.join_sources)
I found a blog post that purports to address this problem: http://blog.donwilson.net/2011/11/constructing-a-less-than-simple-query-with-rails-and-arel/
Based on this (and my own testing), the following should work for your situation:
car = Car.arel_table
engine = Engine.arel_table
sql = car.project(car[Arel.star])
.join(engine, Arel::Nodes::OuterJoin).on(car[:engine_id].eq(engine[:id]))
Car.find_by_sql(sql)
If you don't mind adding a dependency and skipping AREL altogether, you could use Ernie Miller's excellent Squeel gem. It would be something like
Car.joins{engine.outer}.where(...)
This would require that the Car model be associated with Engine like so:
belongs_to :engine

Rails, Ransack: How to search HABTM relationship for "all" matches instead of "any"

I'm wondering if anyone has experience using Ransack with HABTM relationships. My app has photos which have a habtm relationship with terms (terms are like tags). Here's a simplified explanation of what I'm experiencing:
I have two photos: Photo 1 and Photo 2. They have the following terms:
Photo 1: A, B, C
Photo 2: A, B, D
I built a ransack form, and I make checkboxes in the search form for all the terms, like so:
- terms.each do |t|
= check_box_tag 'q[terms_id_in][]', t.id
If I use: q[terms_id_in][] and I check "A, C" my results are Photo 1 and Photo 2. I only want Photo 1, because I asked for A and C, in this query I don't care about B or D but I want both A and C to be present on a given result.
If I use q[terms_id_in_all][] my results are nil, because neither photo includes only A and C. Or, perhaps, because there's only one term per join, so no join matches both A and C. Regardless, I want just Photo 1 to be returned.
If I use any variety of q[terms_id_eq][] I never get any results, so I don't think that works in this case.
So, given a habtm join, how do you search for models that match the given values while ignoring not given values?
Or, for any rails/sql gurus not familiar with Ransack, how else might you go about creating a search form like I'm describing for a model with a habtm join?
Update: per the answer to related question, I've now gotten as far as constructing an Arel query that correctly matches this. Somehow you're supposed to be able to use Arel nodes as ransackers, or as cdesrosiers pointed out, as custom predicates, but thus far I haven't gotten that working.
Per that answer, I setup the following ransack initializer:
Ransack.configure do |config|
config.add_predicate 'has_terms',
:arel_predicate => 'in',
:formatter => proc {|term_ids| Photo.terms_subquery(term_ids)},
:validator => proc {|v| v.present?},
:compounds => true
end
... and then setup the following method on Photo:
def self.terms_subquery(term_ids)
photos = Arel::Table.new(:photos)
terms = Arel::Table.new(:terms)
photos_terms = Arel::Table.new(:photos_terms)
photos[:id].in(
photos.project(photos[:id])
.join(photos_terms).on(photos[:id].eq(photos_terms[:photo_id]))
.join(terms).on(photos_terms[:term_id].eq(terms[:id]))
.where(terms[:id].in(term_ids))
.group(photos.columns)
.having(terms[:id].count.eq(term_ids.length))
).to_sql
end
Unfortunately this doesn't seem to work. While terms_subquery produces the correct SQL, the result of Photo.search(:has_terms => [2,5]).result.to_sql is just "SELECT \"photos\".* FROM \"photos\" "
With a custom ransack predicate defined as in my answer to your related question, this should work with a simple change to your markup:
- terms.each do |t|
= check_box_tag 'q[id_has_terms][]', t.id
UPDATE
The :formatter doesn't do what I thought, and seeing as how the Ransack repo makes not a single mention of "subquery," you may not be able to use it for what you're trying to do, after all. All available options seem to be exhausted, so there would be nothing left to do but monkey patch.
Why not just skip ransack and query the "photos" table as you normally would with active record (or even with the Arel query you now have)? You already know the query works. Is there a specific benefit you hoped to reap from using Ransack?

Rails 3: Building a set of filters on an index view using scopes

I have an index view of a model which I would like to filter by some combination of the model's attributes.
For example, I have a Bill model (not the kind on ducks, the kind you have to pay) that I might filter on payee and/or status.
The model has a scope for each individual attribute, e.g.
scope :bill_status, lambda {|status| where("status = ?", status}
scope :bill_payee, lambda {|payee| where("payee_id = ?", payee.id}
The view allows the user to select zero or more options -- if an option is not selected, it means "don't filter by this".
In the controller, I can do something yucky like this:
def index
status = params[:bill][:status]
payee = params[:bill][:payee]
if status.present? and payee.present?
# chain scopes
#bills = Bill.bill_status(status).bill_payee(payee)
elsif status.present?
#bills = Bill.bill_status(status)
elsif payee.present?
#bills = Bill.bill_payee(payee)
else
#bills = Bill.all
end
# rest of controller action
end
But while this works, it's neither pretty nor easily extensible -- adding a third filter means I now have many more possibilities. I seek beauty and purity.
On the assumption that my scopes are all chainable, it seems like I should be able to do something like
def index
#bills = Bill.all
#bills = #bills.bill_status(params[:bill][:status]) if params[:bill][:status].present?
#bills = #bills.bill_payee(params[:bill][:payee]) if params[:bill][:payee].present?
# rest of controller code
end
'cept it doesn't work because Bill.all is an array. Plus, that's no fun because Bill.all executes the query, which I only want to run once thanks to AREL magic. Should I just define a scope like all_bills (with no conditions?) -- that would be an ActiveRecord::Relation I guess...
Is there a pattern that solves this problem more elegantly? Whenever I have code to do one thing that relies on Model, View and Controller I feel as though I might be doing something wrong. Or as though someone smarter and harder working than I has already solved it :-)
Bonus question: I want this all to work with my paginator of choice, the most excellent Kaminari gem.
All thoughts and ideas welcomed.
I'd do something like this:
proxy = Bill.scoped
if status.present?
proxy = proxy.bill_status(status)
end
if payee.present?
proxy = proxy.bill_payee(payee)
end
#bills = proxy
You can even do then some meta-programming:
#bills = [:status, :payee, ...].inject(Bill.scoped) do |proxy, param|
val = params[:bill][param]
val.present ? proxy.send("bill_#{param}", val) : proxy
end
As I searched for solutions to what seemed like a common problem, I checked out Ryan Bates' RailsCast and found an episode from 3 days ago on Ransack. Ransack is a pretty seriously cool gem for form searching and column sorting, and I think that's the way I am going.
Thanks for the answers here -- I am glad to have learned a couple of great techniques from those who took the time and effort the answer.