I have an ActiveRecord request:
Post.all.select { |p| Date.today < p.created_at.weeks_since(2) }
And I want to be able to see what SQL request this produces using .to_sql
The error I get is: NoMethodError: undefined method 'to_sql'
TIA!
ISSUE
There are 2 types of select when it comes to ActiveRecord objects, from the Docs
select with a Block.
First: takes a block so it can be used just like Array#select.
This will build an array of objects from the database for the scope, converting them into an array and iterating through them using Array#select.
This is what you are using right now. This implementation will load every post instantiate a Post object and then iterating over each Post using Array#select to filter the results into an Array. This is highly inefficient, cannot be chained with other AR semantics (e.g. where,order,etc.) and will cause very long lags at scale. (This is also what is causing your error because Array does not have a to_sql method)
select with a list of columns (or a String if you prefer)
Second: Modifies the SELECT statement for the query so that only certain fields are retrieved...
This version is unnecessary in your case as you do not wish to limit the columns returned by the query to posts.
Suggested Resolution:
Instead what you are looking for is a WHERE clause to filter the records at the database level before returning them to the ORM.
Your current filter is (X < Y + 2)
Date.today < p.created_at.weeks_since(2)
which means Today's Date is less than Created At plus 2 Weeks.
We can invert this criteria to make it easier to query by switching this to Today's Date minus 2 weeks is less than Created At. (X - 2 < Y)
Date.today.weeks_ago(2) < p.created_at
This is equivalent to p.created_at > Date.today.weeks_ago(2) which we can convert to a where clause using standard ActiveRecord query methods:
Post.where(created_at: Date.today.weeks_ago(2)...)
This will result in SQL like:
SELECT
posts.*
FROM
posts.*
WHERE
posts.created_at > '2022-10-28'
Notes:
created_at is a TimeStamp so it might be better to use Time.now vs Date.today.
Additional concerns may be involved from a time zone perspective since you will be performing date/time specific comparisons.
You need to call to_sql on a relation. select executes the query and gives you the result, and on the result you don't have to_sql method.
There are similar questions which you can look at as they offer some alternatives.
Related
How can I use the join table's column value with arithmetic operation during the where condition on Rails?
User and Order are the two Schema, Order has user via Foreign key relation
My goal is to find if an Order was created/placed within 5 minutes of User creation (Understanding Users who signup for placing an Order)
Tried the following queries
Order.where('country': 'US').joins(:user).where('orders.created_at <= :u_date', {u_date: 'users.created_at' + 5.minutes })
With this query we get the following error no implicit conversion of Time into String, so the users.created_at is not evaluating into a Date
Hence tried converting the string to DateTime objects, which failed too
Order.joins(:user).where('orders.created_at < ?', 'users.created_at'+ 5.minutes)
How can I do the comparison inside the Where query?
Right now I am plucking the data and comparing it, It'd be great to make it work inside the Where or any relevant query itself
You're invoking + on a string passing as argument a Time object, which is not an out-of-the-box operation, at least in Rails.
If the time to add is not dynamic you could try;
where("orders.created_at <= users.created_at + INTERVAL '5.minutes'")
which makes your DBMS add the proper interval to users.created_at (in this case I'm assuming Postgresql)
How can I speed up the following query? I'm look to find record with 6 or less unique values of fb_id. The select doesn't seem to be adding much in terms of time but instead it's the group and count. Is there an alternate way to query? I added an index on fb_id and it only sped up the query by 50%
FbGroupApplication.group(:fb_id).where.not(
fb_id: _get_exclude_fb_group_ids
).group(
"count_fb_id desc"
).count(
"fb_id"
).select{|k, v| v <= 6 }
The query is looking for FbGroupApplications that have 6 or less applications to the same fb_id
Passing a block to the select method made Rails trigger the SQL, convert the found rows into ActiveRecord::Base's ruby object (record), and then perform a select on the array based of the block you gave. This whole process is costly (ruby is not good at this).
You can "delegate" the responsibility of comparing the count vs 6 to the database with a having clause:
FbGroupApplication
.group(:fb_id)
.where.not(fb_id: _get_exclude_fb_group_ids)
.having('count(fb_id) <= 6')
I have a model Company that have columns pbr, market_cap and category.
To get averages of pbr grouped by category, I can use group method.
Company.group(:category).average(:pbr)
But there is no method for weighted average.
To get weighted averages I need to run this SQL code.
select case when sum(market_cap) = 0 then 0 else sum(pbr * market_cap) / sum(market_cap) end as weighted_average_pbr, category AS category FROM "companies" GROUP BY "companies"."category";
In psql this query works fine. But I don't know how to use from Rails.
sql = %q(select case when sum(market_cap) = 0 then 0 else sum(pbr * market_cap) / sum(market_cap) end as weighted_average_pbr, category AS category FROM "companies" GROUP BY "companies"."category";)
ActiveRecord::Base.connection.select_all(sql)
returns a error:
output error: #<NoMethodError: undefined method `keys' for #<Array:0x007ff441efa618>>
It would be best if I can extend Rails method so that I can use
Company.group(:category).weighted_average(:pbr)
But I heard that extending rails query is a bit tweaky, now I just want to know how to run the result of sql from Rails.
Does anyone knows how to do it?
Version
rails: 4.2.1
What version of Rails are you using? I don't get that error with Rails 4.2. In Rails 3.2 select_all used to return an Array, and in 4.2 it returns an ActiveRecord::Result. But in either case, it is correct that there is no keys method. Instead you need to call keys on each element of the Array or Result. It sounds like the problem isn't from running the query, but from what you're doing afterward.
In any case, to get the more fluent approach you've described, you could do this:
class Company
scope :weighted_average, lambda{|col|
select("companies.category").
select(<<-EOQ)
(CASE WHEN SUM(market_cap) = 0 THEN 0
ELSE SUM(#{col} * market_cap) / SUM(market_cap)
END) AS weighted_average_#{col}
EOQ
}
This will let you say Company.group(:category).weighted_average(:pbr), and you will get a collection of Company instances. Each one will have an extra weighted_average_pbr attribute, so you can do this:
Company.group(:category).weighted_average(:pbr).each do |c|
puts c.weighted_average_pbr
end
These instances will not have their normal attributes, but they will have category. That is because they do not represent individual Companies, but groups of companies with the same category. If you want to group by something else, you could parameterize the lambda to take the grouping column. In that case you might as well move the group call into the lambda too.
Now be warned that the parameter to weighted_average goes straight into your SQL query without escaping, since it is a column name. So make sure you don't pass user input to that method, or you'll have a SQL injection vulnerability. In fact I would probably put a guard inside the lambda, something like raise "NOPE" unless col =~ %r{\A[a-zA-Z0-9_]+\Z}.
The more general lesson is that you can use select to include extra SQL expressions, and have Rails magically treat those as attributes on the instances returned from the query.
Also note that unlike with select_all where you get a bunch of hashes, with this approach you get a bunch of Company instances. So again there is no keys method! :-)
Rails: 4.1.2
Database: PostgreSQL
For one of my queries, I am using methods from both the textacular gem and Active Record. How can I chain some of the following queries with an "OR" instead of an "AND":
people = People.where(status: status_approved).fuzzy_search(first_name: "Test").where("last_name LIKE ?", "Test")
I want to chain the last two scopes (fuzzy_search and the where after it) together with an "OR" instead of an "AND." So I want to retrieve all People who are approved AND (whose first name is similar to "Test" OR whose last name contains "Test"). I've been struggling with this for quite a while, so any help would be greatly appreciated!
I digged into fuzzy_search and saw that it will be translated to something like:
SELECT "people".*, COALESCE(similarity("people"."first_name", 'test'), 0) AS "rankxxx"
FROM "people"
WHERE (("people"."first_name" % 'abc'))
ORDER BY "rankxxx" DESC
That says if you don't care about preserving order, it will just filter the result by WHERE (("people"."first_name" % 'abc'))
Knowing that and now you can simply write the query with similar functionality:
People.where(status: status_approved)
.where('(first_name % :key) OR (last_name LIKE :key)', key: 'Test')
In case you want order, please specify what would you like the order will be after joining 2 conditions.
After a few days, I came up with the solution! Here's what I did:
This is the query I wanted to chain together with an OR:
people = People.where(status: status_approved).fuzzy_search(first_name: "Test").where("last_name LIKE ?", "Test")
As Hoang Phan suggested, when you look in the console, this produces the following SQL:
SELECT "people".*, COALESCE(similarity("people"."first_name", 'test'), 0) AS "rank69146689305952314"
FROM "people"
WHERE "people"."status" = 1 AND (("people"."first_name" % 'Test')) AND (last_name LIKE 'Test') ORDER BY "rank69146689305952314" DESC
I then dug into the textacular gem and found out how the rank is generated. I found it in the textacular.rb file and then crafted the SQL query using it. I also replaced the "AND" that connected the last two conditions with an "OR":
# Generate a random number for the ordering
rank = rand(100000000000000000).to_s
# Create the SQL query
sql_query = "SELECT people.*, COALESCE(similarity(people.first_name, :query), 0)" +
" AS rank#{rank} FROM people" +
" WHERE (people.status = :status AND" +
" ((people.first_name % :query) OR (last_name LIKE :query_like)))" +
" ORDER BY rank#{rank} DESC"
I took out all of quotation marks in the SQL query when referring to tables and fields because it was giving me error messages when I kept them there and even if I used single quotes.
Then, I used the find_by_sql method to retrieve the People object IDs in an array. The symbols (:status, :query, :query_like) are used to protect against SQL injections, so I set their values accordingly:
# Retrieve all the IDs of People who are approved and whose first name and last name match the search query.
# The IDs are sorted in order of most relevant to the search query.
people_ids = People.find_by_sql([sql_query, query: "Test", query_like: "%Test%", status: 1]).map(&:id)
I get the IDs and not the People objects in an array because find_by_sql returns an Array object and not a CollectionProxy object, as would normally be returned, so I cannot use ActiveRecord query methods such as where on this array. Using the IDs, we can execute another query to get a CollectionProxy object. However, there's one problem: If we were to simply run People.where(id: people_ids), the order of the IDs would not be preserved, so all the relevance ranking we did was for nothing.
Fortunately, there's a nice gem called order_as_specified that will allow us to retrieve all People objects in the specific order of the IDs. Although the gem would work, I didn't use it and instead wrote a short line of code to craft conditions that would preserve the order.
order_by = people_ids.map { |id| "people.id='#{id}' DESC" }.join(", ")
If our people_ids array is [1, 12, 3], it would create the following ORDER statement:
"people.id='1' DESC, people.id='12' DESC, people.id='3' DESC"
I learned from this comment that writing an ORDER statement in this way would preserve the order.
Now, all that's left is to retrieve the People objects from ActiveRecord, making sure to specify the order.
people = People.where(id: people_ids).order(order_by)
And that did it! I didn't worry about removing any duplicate IDs because ActiveRecord does that automatically when you run the where command.
I understand that this code is not very portable and would require some changes if any of the people table's columns are modified, but it works perfectly and seems to execute only one query according to the console.
I was trying to select objects uniq by one attribute, using #videos.uniq{|p| p.author}
time = Time.new(2014, 12)
start_time = time.beginning_of_month
end_time = time.end_of_month
videos = Video.where("created_at > ? AND created_at < ?", start_time, end_time).where("likes > ?", 15)
selected_videos = videos.uniq{|p| p.author}
puts videos.count, videos.class, selected_videos.count
#=> 23, Video::ActiveRecord_Relation, 23
videos_first = videos.first(23)
selected_videos = videos_first.uniq{|p| p.author}
puts videos_first.count, videos_first.class, selected_videos.count
#=> 23, array, 10
.uniq is not for ActiveRecord_Relation. And the problem is that the query returns a Video::ActiveRecord_Relation, but I need array.
Certainly, this could be achieved by using to_a, but is this elegant?
What's the correct way of handling this ?
Is it possible to use .uniq for activerecord:relation?
If you need to access to the query result, just use #to_a on ActiveRecord::Relation instance.
At rails guides you can find on notable changes at Rails 4.0: "Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array. In some specific cases, this may cause breakage when upgrading." That is valid for other relation methods like :where.
selected_videos = videos.to_a.uniq{|p| p.author}
.uniq does not make much sense when it is applied across the full active-record record.
Given that at least one or more of the three attributes - id, created_at, and updated_at - are different for every row, applying videos.uniq{|p| p.author} where videos is a ActiveRecord::Relation including all fields, will return all the rows in the ActiveRecord::Relation.
When the ActiveRecord::Relation object has a subset of values, uniq will be able to figure out the distinct values from them.
Eg: videos.select(:author).uniq.count will give 10 in your example.
The difference between ActiveRecord::Relation#uniq and Array#uniq is that the Array version accepts a block and uses the return value of a block for comparison. The ActiveRecord::Relation version of uniq simply ignores the block.
If you need the records, you can use ActiveRecord::Relation#load
Causes the records to be loaded from the database if they have not been loaded already. You can use this if for some reason you need to explicitly load some records before actually using them. The return value is the relation itself, not the records.
https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-load