Rails: Conditional subquery MAX(effective_date) - ruby-on-rails

Let's suppose, there are three tables in the database:
courses (:id, :name)
course_details (:course_id, :effective_date, :status),
course_codes (:course_detail_id, :code)
course has many course_details
course_detail has many cource_codes
Course can have multiple course_details records which are effective_dated applicable (means only one record of course_detail will be used in the system).
Problem statement: I want to filter courses by course codes given code. And course should only be filtered by the course_codes which are linked with effective dated course_detail and should skip the past effective dated records.
course = Course.find(params[:id])
course_detail = CourseDetail.find_by(effective_date: CourseDetail.max(effective_date), course_id: course.id)
If I use this code this will filter course irrespective of effective_dated course_details:
Course.left_joins(course_details: :course_codes).where(course_details: { course_codes: { code: params[:code] } })
courses:
Id
Name
1
English
2
Maths
course_details:
id
course_id
effective_date
1
1
2020-10-01
2
1
2021-01-01
3
2
2020-09-01
course_codes:
id
course_detail_id
code.
1
1
eng-01
2
2
eng-505
3
3
math-01
when I pass code = eng-01 it should return empty array instead of course with id 1.
Can somebody please help me?

To resolve this issue, I used a subquery that returns ids of course_details of all the courses according to effective_date:
query = "select child.id from courses as parent
inner join course_details as child on child.course_id = parent.id
where child.effective_date =
(select max(child1.effective_date) as effective_date
from course_details as child1
where child1.course_id = parent.id
and (child1.effective_date <= CURRENT_DATE
or child1.effective_date = (select min(child2.effective_date) as effective_date
from course_details as child2
where child2.course_id = parent.id)
))"
effective_dated_ids = Course.find_by_sql(query).pluck(:id)
After getting all the ids, I passed these ids in search.
records = Course.left_joins(course_details: :course_codes).where(course_details: { id: effective_record_ids, course_codes: { course_code: params[:course_code] } })
And it worked as expected.

Related

ActiveRecord joins - Return only exact matches

I have three models, Outfit, Product, and a join model OutfitProduct (Outfit has many Products through OutfitProducts).
I would like to find outfits that contain only exact product matches.
So far I have this
def by_exact_products(products)
joins(outfit_products: :product)
.where(outfit_products: { product: products })
.group("outfits.id")
.having('count(outfits.id) = ?', products.size)
end
The above returns any outfit that contains the products I am searching for, even if it is not an exact match. I would like it to return only outfits that are an exact match.
Example:
Assume we have the following outfits made up of the following products:
outfit_1.products = [product_1, product_2, product_3, product_4]
outfit_2.products = [product_1, product_2]
outfit_3.products = [product_1, product_2, product_3]
If I passed [product_1, product_2, product_3] to my query, it will return outfit_1 and outfit_3 - I would like it to only return outfit_3 which is an exact match.
UPDATE (More info)
Calling the query with an array of three products produces the following query:
SELECT "outfits".*
FROM "outfits"
INNER JOIN "outfit_products"
ON "outfit_products"."outfit_id" = "outfits"."id"
INNER JOIN "products"
ON "products"."id" = "outfit_products"."product_id"
WHERE "outfit_products"."product_id" IN ( 18337, 6089, 6224 )
GROUP BY outfits.id
HAVING ( Count(outfits.id) = 3 )
Let's first have a look at why this is happening. You use the following scenario:
outfit_1.products = [product_1, product_2, product_3, product_4]
outfit_2.products = [product_1, product_2]
outfit_3.products = [product_1, product_2, product_3]
This would have the following outfit_products table:
outfit_id | product_id
----------|-----------
1 | 1
1 | 2
1 | 3
1 | 4
2 | 1
2 | 2
3 | 1
3 | 2
3 | 3
When you add the restriction:
WHERE "outfit_products"."product_id" IN ( 1, 2, 3 )
It will eliminate the row:
outfit_id | product_id
----------|-----------
1 | 4
And leave product 1 with 3 records, when you group and count the records you'll end up with a resulting value of 3 for product 1. This means the current query will only check for a minimum of the provided products (aka make sure all the provided products are present).
To also eliminate records that have more products than the provided products you'll have to add a second count. Which counts the products without the above restriction.
def by_exact_products(products)
# all outfits that have at least all products
with_all_products = joins(outfit_products: :product)
.where(outfit_products: { product: products })
.group("outfits.id")
.having('count(outfits.id) = ?', products.size)
# all outfits that have exactly all products
joins(outfit_products: :product)
.where(id: with_all_products.select(:id))
.group("outfits.id")
.having('count(outfits.id) = ?', products.size)
end
This will select all outfits that have have at least all provided products, and count their product total.

How to efficiently retrieve groups of objects from activerecord with a single SQL query?

I have a table products which has a product_type_code column on it. What I'd like to do is retrieve different numbers of objects based on this column (eg.: 3 products with product_type_code = 'fridge', 6 products with product_type_code = 'car', 9 products with product_type_code = 'house', etc.).
I know I can do like this:
fridges = Product.where(product_type_code: 'fridge').limit(3)
houses = Product.where(product_type_code: 'house').limit(9)
[...]
And even create a scope like this:
# app/models/product.rb
scope :by_product_type_code, -> (material) { where(product_type_code: product_type_code) }
However, this is not efficient since I go to the database 3 times, if I'm not wrong. What I'd like to do is something like:
scope :by_product_type_code, -> (hash) { some_method(hash) }
where hash is: { fridge: 3, car: 6, house: 9 }
and get an ActiveRecord_Relation containing 3 fridges, 6 cars and 9 houses.
How can I do that efficiently?
You can create a query using UNION ALL, which selects records having a specifc product_type_code and limit to use it with find_by_sql:
{ fridge: 3, car: 6, house: 9 }.map do |product_type_code, limit|
"(SELECT *
FROM products
WHERE product_type_code = '#{product_type_code}'
LIMIT #{limit})"
end.join(' UNION ALL ')
And you're gonna have a query like:
(SELECT * FROM products WHERE product_type_code = 'fridge'LIMIT 3)
UNION ALL
(SELECT * FROM products WHERE product_type_code = 'car'LIMIT 6)
UNION ALL
(SELECT * FROM products WHERE product_type_code = 'house'LIMIT 9)
#SebastianPalma's answer is the best solution; however if you were looking for a more "railsy" fashion of generating this query you can use arel as follows:
scope :by_product_type_code, ->(h) {
products_table = self.arel_table
query = h.map do |product_type,limit|
products_table.project(:id)
.where(products_table[:product_type_code].eq(product_type))
.take(limit)
end.reduce do |scope1, scope2|
Arel::Nodes::UnionAll.new(scope1,scope2)
end
self.where(id: query)
end
This will result in the sub query being part of the where clause.
Or
scope :by_product_type_code, ->(h) {
products_table = self.arel_table
query = h.map do |product_type,limit|
products_table.project(Arel.star)
.where(products_table[:product_type_code].eq(product_type))
.take(limit)
end.reduce do |scope1, scope2|
Arel::Nodes::UnionAll.new(scope1,scope2)
end
sub_query = Arel::Nodes::As.new(query,products_table)
self.from(sub_query)
end
This will result in the subquery being the source of the data.

Scope Order by Count with Conditions Rails

I have a model Category that has_many Pendencies. I would like to create a scope that order the categories by the amount of Pendencies that has active = true without excluding active = false.
What I have so far is:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).order('COUNT(pendencies.id) DESC')}
This will order it by number of pendencies, but I want to order by pendencies that has active = true.
Another try was:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).where('pendencies.active = ?', true).order('COUNT(pendencies.id) DESC')}
This will order by number of pendencies that has pendencies.active = true, but will exclude the pendencies.active = false.
Thank you for your help.
I guess you want to sort by the amount of active pendencies without ignoring categories that have no active pendencies.
That would be something like:
scope :order_by_pendencies, -> {
active_count_q = Pendency.
group(:category_id).
where(active: true).
select(:category_id, "COUNT(*) AS count")
joins("LEFT JOIN (#{active_count_q.to_sql}) AS ac ON ac.category_id = id").
order("ac.count DESC")
}
The equivalent SQL query:
SELECT *, ac.count
FROM categories
LEFT JOIN (
SELECT category_id, COUNT(*) AS count
FROM pendencies
GROUP BY category_id
WHERE active = true
) AS ac ON ac.category_id = id
ORDER BY ac.count DESC
Note that if there are no active pendencies for a category, the count will be null and will be added to the end of the list.
A similar subquery could be added to sort additionally by the total amount of pendencies...
C# answer as requested:
method() {
....OrderBy((category) => category.Count(pendencies.Where((pendency) => pendency.Active))
}
Or in straight SQL:
SELECT category.id, ..., ActivePendnecies
FROM (SELECT category.id, ..., count(pendency) ActivePendnecies
FROM category
LEFT JOIN pendency ON category.id = pendency.id AND pendnecy.Active = 1
GROUP BY category.id, ...) P
ORDER BY ActivePendnecies;
We have to output ActivePendnecies in SQL even if the code will throw it out because otherwise the optimizer is within its rights to throw out the ORDER BY.
For now I developed the following (it's working, but I believe that it's not the best way):
scope :order_by_pendencies, -> { scoped = Category.left_joins(:pendencies)
.group(:id)
.order('COUNT(pendencies.id) DESC')
.where('pendencies.active = ?', true)
all = Category.all
(scoped + all).uniq}

List unique line items and their daily counts

In my rails app, new line items are created daily. I need to be able to have my smart_listing show how many apples and oranges were ordered. For instance:
Line Item QTY
Apple 2
Orange 1
What I am getting is:
Line Item QTY
Apple 1
Apple 1
Orange 1
line_item_scope = LineItem.all
line_item_scope = line_item_scope.where(created_at: Date.today.beginning_of_day..Date.today.end_of_day)
if customer_signed_in?
line_item_scope = line_item_scope.ticket.customer(current_customer.id)
end
#line_items = smart_listing_create(:line_items, line_item_scope, partial: "line_items/listing2", default_sort: {updated_at: "desc"})
My initial thought was to create a .map(&:name).uniq but that returns an array when I need a relationship to go into the smart listing.
If you need to display just LineItem's name and the number of items of that name, then group method can help:
line_item_scope.group(:name).count
This will construct a hash:
result = { "Apple" => 2, "Orange" => 1 }
Then this hash can be iterated to display the values:
result.each do |name, count|
...
end
Or the number of line items can be selected as a column:
line_items_scope =
LineItem.group(:name)
.order(:name)
.select("name, COUNT(*) as count")
Then line_items_scope can be fed to smart_listing_create as a ActiveRecordRelation

Rails query association AND

I have an User model with a HABTM association with Tag model. I need all users who necessarily have all conditions, not just one.
Ex:
User.includes(:tags).where(tags: { id: [2,3,...] })
Returns users who have tags with id 2 and / or 3, but I would like to only return users who have tags with ids 2 AND 3.
I can think of this option:
User.includes(:tags).where(tags: { id: 2 }).where(tags: { id: 3 })
If you have a severals tag_ids, and if the intermediate table between user and tags is user_tags
tag_ids = [1, 2, 3, 4, 5]
where = tag_ids.map do |id|
"tags.id = #{id}"
end.join(" AND ")
puts where # "tags.id = 1 AND tags.id = 2 AND tags.id = 3 AND tags.id = 4 AND tags.id = 5"
User.joins("INNER JOIN user_tags ON user_tags.user_id = user.id INNER JOIN tags ON tags.id = user_tags.tag_id").where(where)
If you wish to continue using the includes, there is no other choice that continue using the where in the rails-ish way.
How about:
user_ids = Tag.where(id: [2,3]).pluck(:user_id).uniq
User.where(id: user_id)
pluck won't instantiate all the tag objects, so while this may not be ideal, it should at least be pretty quick.

Resources