Raw SQL to Rails ActiveRecord - ruby-on-rails

Because I'm newer to Rails than I am SQL, I took a shortcut in order to build a fairly complicated (to me, anyways) query.
Good news: I have the query working
Bad news: I'm stuck with hackish code AND can't figure out how to drop dynamic variables into it (which should be easy if it as in Rails-friendly format).
Here's my query (broken up for ease of reading):
ActiveRecord::Base.connection.select_all( "SELECT DISTINCT ps.* FROM merchants merch, product_sales ps, users u, user_merchant_relations umr, memberships m
INNER JOIN marketing_efforts me ON me.id = ps.marketing_effort_id
LEFT OUTER JOIN member_sales ms ON me.id = ms.marketing_effort_id
LEFT OUTER JOIN crm_sales cs ON me.id = cs.marketing_effort_id
WHERE ((me.start < '2013-05-26' AND me.end > '2013-05-26') OR (me.start < '2013-05-26' AND me.end IS NULL)) AND (me.num_avail IS NULL OR me.num_avail > 0) AND (me.gender IS NULL OR me.gender = u.gender OR u.gender IS NULL) AND ((u.birthdate > me.birthdate_start AND u.birthdate < me.birthdate_end) OR (u.birthdate > me.birthdate_start AND me.birthdate_end IS NULL) OR (me.birthdate_start IS NULL AND me.birthdate_end IS NULL) OR u.birthdate IS NULL) AND ((ms.member_id = m.member_id AND m.user_id = u.id) OR ms.member_id IS NULL) AND (cs.crm_tier_id = umr.crm_tier_id OR cs.crm_tier_id IS NULL) AND u.id = 3 AND merch.id = 1")
Anywhere '2013-05-26' appears I need to drop in Time.now. The u.id and merch.id at the very end also need to accept variables.
I thought I could simply do: .S=?...Q=?....L=?...", value, value, value) but that's not working.
Update
I'm here:
ProductSale.joins('INNER JOIN marketing_efforts ON marketing_efforts.id = product_sales.marketing_effort_id').joins('LEFT OUTER JOIN member_sales ON marketing_efforts.id = member_sales.marketing_effort_id').joins('LEFT OUTER JOIN crm_sales ON marketing_efforts.id = crm_sales.marketing_effort_id')
So I'm taking care of all the joins, but the problem I'm running into is that, with the raw SQL, I could easily define all the tables I need to access ... e.g.:
FROM merchants merch, product_sales ps, users u, user_merchant_relations umr, memberships m
Whereas I'm not sure how to do this with Rails. As a result, I just get FROM product_sales :
ProductSale Load (0.3ms) SELECT "product_sales".* FROM "product_sales" INNER JOIN ....
So when I try to say, for example, users.id = 1 I get:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.id:
Any thoughts? Thank you.

Perhaps not the best way of doing it, but it works and solved the main issue (inability to pass dynamic variables):
ProductSale.find_by_sql [ " QUE=?...R=?..Y=? ", value, value, value]

Related

how to write a join condition in Ruby on Rails?

what I'm trying to do is to write something like the next query:
SELECT *
FROM Customers c
LEFT JOIN CustomerAccounts ca
ON ca.CustomerID = c.CustomerID
AND c.State = 'NY'
Notice that I'm not using any WHERE clause, but I need to my JOIN have a condition. I cannot make it work in Ruby on Rails.
Can you help me out?
You can join the tables with LEFT JOIN. Just pass the join condition in joins and you will get the expected result
Customer.joins("LEFT JOIN CustomerAccounts
ON CustomerAccounts.CustomerID = Customers.CustomerID
AND Customers.State = 'NY'")
#=> SELECT * FROM Customers LEFT JOIN CustomerAccounts ON CustomerAccounts.CustomerID = Customers.CustomerID AND Customers.State = 'NY'
Note: just .joins() does INNER JOIN so you need to specify the join with condition
Your SQL code, translated to activerecord, would look as follows (using joins):
Customer.where(state: 'NY').joins(:customer_accounts)
The code assumes, you have the association set up:
class Customer
has_many :customer_accounts
end

spree 3.0 only show products that have available stock

So i see this question has been asked before but the answer wasn't posted even though the question got answered, and since i dont have enough points to comment on the answer i need to ask it again. here is the original post
Is there a scope to allow me to show only products with variants with stock?
This is for spree 3.0. when i added the shown code
joins(
variants_including_master: :stock_items
).group('spree_products.id').having("SUM(count_on_hand) > 0")
to my product decorator like such
def self.available(available_on = nil, currency = nil)
# joins(:master => :prices).where("#{Product.quoted_table_name}.available_on <= ?", available_on || Time.now)
joins(:master => :prices).where("#{Product.quoted_table_name}.available_on <= ?", available_on || Time.now).joins(variants_including_master: :stock_items).group('spree_products.id').having("SUM(count_on_hand) > 0")
end
search_scopes << :available
this blows up the view since the result has some strange properties due to the GROUP BY as far as i can tell. this errors out in the products_helper.rb file on the line
max_updated_at = (#products.maximum(:updated_at) || Date.today).to_s(:number)
because #products.maximum(:updated_at) returns a hash
{[6, 6]=>2015-07-04 19:08:36 UTC, [3, 3]=>2015-07-04 19:07:37 UTC, [10, 10]=>2015-07-07 19:01:12 UTC}
instead of returning a date
2015-07-07 19:01:12 UTC
which it does normally.
this code also blows up on the taxon page with the following error:
PG::GroupingError: ERROR: column "spree_products_taxons.position" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT DISTINCT "spree_products"."id", spree_products_taxon...
^
: SELECT DISTINCT "spree_products"."id", spree_products_taxons.position AS alias_0 FROM "spree_products" INNER JOIN "spree_variants" ON "spree_variants"."product_id" = "spree_products"."id" AND "spree_variants"."is_master" = 't' AND "spree_variants"."deleted_at" IS NULL INNER JOIN "spree_prices" ON "spree_prices"."variant_id" = "spree_variants"."id" AND "spree_prices"."deleted_at" IS NULL INNER JOIN "spree_variants" "variants_including_masters_spree_products" ON "variants_including_masters_spree_products"."product_id" = "spree_products"."id" AND "variants_including_masters_spree_products"."deleted_at" IS NULL INNER JOIN "spree_stock_items" ON "spree_stock_items"."variant_id" = "variants_including_masters_spree_products"."id" AND "spree_stock_items"."deleted_at" IS NULL LEFT OUTER JOIN "spree_products_taxons" ON "spree_products_taxons"."product_id" = "spree_products"."id" WHERE "spree_products"."deleted_at" IS NULL AND ("spree_products".deleted_at IS NULL or "spree_products".deleted_at >= '2015-07-09 14:30:53.668422') AND ("spree_products".available_on <= '2015-07-09 14:30:53.669089') AND "spree_products_taxons"."taxon_id" IN (2, 7, 14) AND (spree_prices.amount IS NOT NULL) AND "spree_prices"."currency" = 'USD' GROUP BY spree_products.id HAVING SUM(count_on_hand) > 0 ORDER BY spree_products_taxons.position ASC LIMIT 12 OFFSET 0
SO there must be a better way to do this so that the GROUP by clause doesn't throw everything off.
Try something like this:
joins(:master => :prices, variants: :stock_items).where("#{StockItem.quoted_table_name}.count_on_hand > 0 AND #{Product.quoted_table_name}.available_on <= ?", available_on || Time.now).uniq
You might want to include backorderable and track_inventory too - I don't know your setup, maybe you don't have such variants at all.
Edit: Added uniq. If you want master variants too then just change variants: :stock_itemspart to variants_including_master: :stock_items.

Where clause filtering by "ANY" - What does this means?

I'm trying to understand why I don't receive any records on a ruby on rails app using postgresql. This is the SQL query that is being executed:
SELECT g.program_id, g.title,
COALESCE(COUNT(pr), 0) AS ac, g.default
FROM groups AS g
LEFT OUTER JOIN memberships AS m ON m.group_id = g.id
LEFT OUTER JOIN progresses AS pr ON m.id = pr.participant_id
AND (pr.status = 'completed')
WHERE g.program_id = ANY(#1)
GROUP BY g.id
ORDER BY g.program_id, g.position, g.id
My question is: what does the ANY(#1) means?
Please have pacience, as I'm a ruby/rails/postgresql newbie.
Thanks!
Update: added some aditional code. Plese don't ident the query below as it is already idented above.
class StatsComponents::CompletedActivitiesPerGroupStats
include StatsComponent::Interface
GROUP_ACTIVITIES = <<-SQL
g.program_id, g.title, COALESCE(COUNT(pr), 0) AS ac, g.default
FROM groups AS g
LEFT OUTER JOIN memberships AS m ON m.group_id = g.id
LEFT OUTER JOIN progresses AS pr ON m.id = pr.participant_id
AND (pr.status = 'completed')
WHERE g.program_id = ANY(#1)
GROUP BY g.id
ORDER BY g.program_id, g.position, g.id
SQL
def generate
...
It selects records where g.program_id has a value existing in the array returned by #1 request, which they set as a query parameter (for example SELECT...) somewhere further in the program.
You can equally use SOME(#1) here.
By the way strictly speaking, this isn't a sql query. While there is no sql.execute or something like that, it's just a multiline string assignment.

Define left join conditions when using .includes in ActiveRecord

I am trying to find the best way to include a referenced model on what is essentially a compound key.
I have ChecklistItem (a list of things to do daily) and then ChecklistChecks (which ties the ChecklistItem together with a User for a particular day. These checklists can either be for all Stores (with a null store_id) or for a particular Store.
This pulls all of the ChecklistItems and their associated checks:
ChecklistItem.includes(:checklist_checks).where(store_id: [nil,#store.id], list_type: 'open')
The problem is that there would be checks from multiple days in there. What I need is to pull all of the ChecklistItems and any checks from a specific day.
I tried adding conditions like this:
ChecklistItem.includes(:checklist_checks).where(store_id: [nil,#store.id], list_type: 'open', checklist_checks: {store_id: #store.id, report_date: #today})
The problem is that will only pull ChecklistItems that have an associated ChecklistCheck.
It is generating SQL that is essentially:
SELECT
checklist_items.*,
checklist_checks.*
FROM
checklist_items
LEFT OUTER JOIN
checklist_checks
ON
checklist_checks.checklist_item_id = checklist_items.id
WHERE
checklist_items.list_type = 'open'
AND
checklist_checks.store_id = 1
AND
checklist_checks.report_date = '2015-05-03'
AND
(checklist_items.store_id = 1 OR checklist_items.store_id IS NULL)
I think the problem is that the conditions on checklist_checks is in the WHERE clause. If I could move them to the ON clause of the join, everything would work.
Is there a Rails way to end up with something like this?
SELECT
checklist_items.*,
checklist_checks.*
FROM
checklist_items
LEFT OUTER JOIN
checklist_checks
ON
checklist_checks.checklist_item_id = checklist_items.id
AND
checklist_checks.store_id = 1
AND
checklist_checks.report_date = '2015-05-03'
WHERE
checklist_items.list_type = 'open'
AND
(checklist_items.store_id = 1 OR checklist_items.store_id IS NULL)
UPDATE:
I found this: enter link description here
It suggests using find_by_sql and then passing the result array and model to be included to ActiveRecord::Associations::Preloader.new.preload
I tried that, and my find_by_sql pulls the right stuff, but the id column is nil in the resulting objects.
#store = Store.find(1)
#today = Date.today - 1.days
#open_items = ChecklistItem.find_by_sql(["SELECT checklist_items.*, checklist_checks.* FROM checklist_items LEFT OUTER JOIN checklist_checks ON checklist_checks.checklist_item_id = checklist_items.id AND checklist_checks.store_id = ? AND checklist_checks.report_date = ? WHERE checklist_items.list_type='open' AND (checklist_items.store_id=? OR checklist_items.store_ID IS NULL)", #store.id, #today, #store_id])
ActiveRecord::Associations::Preloader.new.preload(#open_items, :checklist_checks)
> #open_items.first.name
=> "Turn on the lights"
> #open_items.first.id
=> nil
A solution using Arel to generate a custom join clause:
class ChecklistItem < ActiveRecord::Base
has_many :checklist_checks
# ...
def self.superjoin(date, store_id)
# build the ON clause for the join
on = Arel::Nodes::On.new(
Arel::Nodes::Equality.new(ChecklistChecks.arel_table[:checklist_item_id], ChecklistItem.arel_table[:id]).\
and(ChecklistItem.arel_table[:store_id].eq(1)).\
and(ChecklistChecks.arel_table[:report_date].eq(date))
)
joins(Arel::Nodes::OuterJoin.new(ChecklistChecks.arel_table, on))
.where(store_id: [nil, store_id], list_type: 'open' )
end
end
I bundled it up into a model method to make it easier to test in the rails console.
irb(main):117:0> ChecklistItem.superjoin(1,2)
ChecklistItem Load (0.5ms) SELECT "checklist_items".* FROM "checklist_items" LEFT OUTER JOIN "checklist_checks" ON "checklist_checks"."checklist_item_id" = "checklist_items"."id" AND "checklist_items"."store_id" = 1 AND "checklist_checks"."report_date" = 1 WHERE (("checklist_items"."store_id" = 2 OR "checklist_items"."store_id" IS NULL)) AND "checklist_items"."list_type" = 'open'
=> #<ActiveRecord::Relation []>

problem: activerecord (rails3), chaining scopes with includes

In Rails3 there seems to be a problem when chaining two scopes (ActiveRelations) that each have a different include:
Consider these two scopes, both of which work fine on their own:
First scope:
scope :global_only, lambda { |user|
includes(:country)
.where("countries.area_id <> ?", user.area) }
Work.global_only(user) => (cut list of fields from SQL for legibility)
SELECT * FROM "works" LEFT OUTER JOIN "countries" ON "countries"."id" = "works"."country_id" WHERE (countries.area_id <> 3)
Now the second scope:
scope :not_belonging_to, lambda { |user|
includes(:participants)
.where("participants.user_id <> ? or participants.user_id is null", user) }
Work.not_belonging_to(user) => (cut list of fields from SQL for legibility)
SELECT * FROM "works" LEFT OUTER JOIN "participants" ON "participants"."work_id" = "works"."id" WHERE (participants.user_id <> 6 or participants.user_id is null)
So both of those work properly individually.
Now, chain them together:
Work.global_only(user).not_belonging_to(user)
The SQL:
SELECT (list of fields) FROM "works" LEFT OUTER JOIN "countries" ON "countries"."id" = "works"."country_id" WHERE (participants.user_id <> 6 or participants.user_id is null) AND (countries.area_id <> 3)
As you can see, the join from the second scope is ignored altogether. The SQL therefore fails on 'no such column 'participants.user_id'. If I chain the scopes in the reverse order, then the 'participants' join will be present and the 'countries' join will be lost. It's always the second join that is lost, it seems.
Does this look like a bug with ActiveRecord, or am I doing something wrong, or is this a "feature" :-)
(PS. Yes, I know, I can create a scope that joins both tables and it will correctly yield the result I want. I have that already. But I was trying to make smaller scopes than can be chained together in different ways, which is supposed to be the advantage of activerecord over straight sql.)
As a general rule, use :includes for eager-loading and :joins for conditions. In the second scope, the join SQL must be manually written because a left join is required.
That said, try this:
scope :global_only, lambda { |user|
joins(:country).
where(["countries.area_id != ?", user.area])
}
scope :not_belonging_to, lambda { |user|
joins("left join participants on participants = #{user.id}").
where("participants.id is null")
}
Work.global_only(user).not_belonging_to(user)

Resources