no id for model? - sql

I get the error
undefined local variable or method `id' for #<Class:0x007fe1dc4e3bb0>
when defining a sql call in my model user:
RECENT_EVENTS_CONDITION = "(user_id = #{id}) OR user_id IN (SELECT user_b_id AS user_id FROM user_follows WHERE user_follows.user_a_id = #{id} )"
Model code
has_many :recent_events,
:class_name => "Activity",
:finder_sql => 'SELECT activities.* FROM activities
WHERE ' + RECENT_EVENTS_CONDITION + '
ORDER BY activities.created_at DESC',
:counter_sql => 'SELECT COUNT(*) FROM activities
WHERE ' + RECENT_EVENTS_CONDITION
Everything works fine until i introduce this recent_events ...

I'm assuming this is a User class or something similar, and I'm assuming when you use #{id}, you're really just trying to use the user's id. So I would do this instead:
RECENT_EVENTS_CONDITION = "(user_id = users.id) OR user_id IN (SELECT user_b_id AS user_id FROM user_follows WHERE user_follows.user_a_id = users.id )"

Related

Implementing a filter for a SQL query in a Rails app

This is a rails app that uses Vue as front-end. There are 2 similar models/tables, named user_notifications and seller_notifications, that have the same columns.
In the user_notifications_controller there are 2 distinct actions, one that shows only user_notifications (for the My Notifications page) and another that shows all notifications (user+seller) [for the Notifications page].
I was not able to have the query results show what I need using ActiveRecords helpers, so I decided to make it directly as an SQL query. This is the original query:
(
SELECT sn.id, sellers.name, sn.seller_id AS member_id, sn.title, sn.body, sn.created_at, sn.is_new, sellers.platform_id, sn.pending_action_id, 'seller_notifications' AS type
FROM seller_notifications sn
INNER JOIN sellers ON sellers.id = sn.seller_id
WHERE sn.platform_id = :platform_id
ORDER BY sn.created_at DESC
) UNION ALL (
SELECT un.id, users.name, un.user_id AS member_id, un.title, un.body, un.created_at, un.is_new, users.platform_id, un.pending_action_id, 'user_notifications' AS type
FROM user_notifications un
INNER JOIN users ON users.id = un.user_id
WHERE un.platform_id = :platform_id
ORDER BY un.created_at DESC
)
LIMIT :limit OFFSET :offset
The issue is that I need to include a filter, which I have done and it works fine for the most part except one: the type (where table's name is), because I need to be able to filter for what kind of notification it is.
Below is the complete code, including query sanitizer:
query = "(
SELECT sn.id, sellers.name, sn.seller_id AS member_id, sn.title, sn.body, sn.created_at, sn.is_new, sellers.platform_id, sn.pending_action_id, 'seller_notifications' AS type FROM seller_notifications sn
INNER JOIN sellers ON sellers.id = sn.seller_id
WHERE sn.platform_id = :platform_id #{params[:is_new].nil? ? nil : ' AND sn.is_new = :is_new '}
#{params[:member_id].nil? ? nil : ' AND sn.seller_id = :member_id '}
#{params[:title].nil? ? nil : ' AND sn.title = :title '}
ORDER BY sn.created_at DESC)
UNION ALL
(SELECT un.id, users.name, un.user_id AS member_id, un.title, un.body, un.created_at, un.is_new, users.platform_id, un.pending_action_id, 'user_notifications' AS type FROM user_notifications un
INNER JOIN users ON users.id = un.user_id
WHERE un.platform_id = :platform_id #{params[:is_new].nil? ? nil : ' AND un.is_new = :is_new '}
#{params[:member_id].nil? ? nil : ' AND un.user_id = :member_id '}
#{params[:title].nil? ? nil : ' AND un.title = :title '}
ORDER BY un.created_at DESC)
LIMIT :limit OFFSET :offset"
sanitizer_ary = [
query,
platform_id: #user.platform_id,
limit: limit,
offset: offset,
member_id: params[:member_id],
title: params[:title], name: params[:name],
type: params[:type],
is_new: params[:is_new]
]
sanitized_query = ActiveRecord::Base.send(:sanitize_sql_array, sanitizer_ary)
result = ActiveRecord::Base.connection.execute(sanitized_query)
result.map { |notif| notif }
When I originally tried to query it as WHERE type = :type it raises an PG::Error that I believe it's because of the lack of table prefix, e.g. un.type instead of just type, but there is no column named type. I have also tried placing the WHERE and ORDER BY statements outside of the parenthesis at the end, just before LIMIT, but it also raises PostgreSQL statement error.
So far, none of what I tried has worked.
ERRORS
This is the original error:
PG::UndefinedColumn: ERROR: column "type" does not exist
LINE 3: WHERE sn.platform_id = 1 AND type = NULL
^
excluded from capture: DSN not set
ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column "type" does not exist
LINE 3: WHERE sn.platform_id = 1 AND type = NULL
^
):
And this is when I tried to use the WHERE statement outside of the parenthesis:
PG::SyntaxError: ERROR: syntax error at or near "WHERE"
LINE 14: WHERE type = NULL
^
excluded from capture: DSN not set
ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: syntax error at or near "WHERE"
LINE 14: WHERE type = NULL
^
):
EXPECTATION
What I need is to be able to include a line, similar to the one below, just after the params[:title] line:
#{params[:type].nil? ? nil : ' AND type = :type'}
Does anybody know how to solve this, i.e.: filter a query result that includes the name of the table by the name of the table itself, which is not included in the table's columns??
I would consider creating a materialized view of the union of the two tables.
CREATE MATERIALIZED VIEW unified_notifications AS
SELECT
sn.id,
sellers.name,
sn.seller_id AS member_id,
sn.title,
sn.body,
sn.created_at,
sn.is_new,
sellers.platform_id,
sn.pending_action_id,
'seller_notifications' AS type
FROM seller_notifications sn
INNER JOIN sellers ON sellers.id = sn.seller_id
UNION ALL
SELECT
un.id,
users.name,
un.user_id AS member_id,
un.title,
un.body,
un.created_at,
un.is_new,
users.platform_id,
un.pending_action_id,
'user_notifications' AS type
FROM user_notifications un
INNER JOIN users ON users.id = un.user_id
The scenic gem can be used to create views with Rails migrations. Note that you need to switch to SQL schema dumps.
Since the view behaves like a table (at least from the perspective of ActiveRecord) you can just create an ActiveRecord model and query it like it was a single table:
class UnifiedNotification < ApplicationRecord
def readonly?
true
end
end
UnifiedNotification.where(
platform_id: #user.platform_id,
# ...
)
If you express your relations cleanly using ActiveRecord, then the queries will express themselves..
class SellerNotification < ApplicationRecord
belongs_to :platform
belongs_to :seller, class_name: "Seller", inverse_of: :notifications
scope :plaftorms, ->(*p) {
p = p.flatten.compact.uniq
p.any? where(plaform_id: p) : none
}
scope :title, ->(title) {
where(title: title)
}
scope :members, ->(*members) {
members = members.flatten.compact.uniq
members.any? ? where(user_id: members) : none
}
end
class Seller < ApplicationRecord
has_many :notifications, class_name: "SellerNotification", dependent: :destroy
scope :platforms, ->(*p) {
joins(:nofifications)
.merge(SellerNotification.platforms(*p))
}
end

Join multiple tables rails sql query

class Account
has_many :metadata
class Keys
has_many :metadata
class Metadatum
belongs_to :key
belongs_ to :account
In Metadatum object I keep additional information about Account, for exapmle 'age'.
In Key object I keep information about type of Metadatum.
Metadatum table:
id
key_id
account_id
value
Key table:
name
data_type
I want to search accounts by multiple metadata. For example
Metadatum with value = '18' that belongs to Key with name = 'age'
Metadatum with value = 'John' that belongs to Key with name = 'first_name'
My query is:
accounts.joins(metadata: :key).where("keys.name = ? AND
metadata.value = ?", params[:key], params[:value]).where("keys.name
= ? AND metadata.value = ?", params[:key1], params[:value1])
It is wrong because in my opinion it looks for an Account that has Metadatum with both key_ids and values. No such Metadatum exists - each has only one key_id and value.
What would be the right query?
If you want accounts that comply with both conditions, I would try:
first = Account.joins(metadata: :key).where("keys.name = ? AND metadata.value = ?", params[:key], params[:value])
final = first.joins(metadata : :key).where("keys.name
= ? AND metadata.value = ?", params[:key1], params[:value1])
New try (really ugly :))
first_ids = Account.joins(metadata: :key).where("keys.name = ? AND metadata.value = ?", params[:key], params[:value]).pluck(“accounts.id”)
final = Account.where(id: first_ids).joins(metadata : :key).where("keys.name = ? AND metadata.value = ?", params[:key1], params[:value1])
Try following
accounts.joins(metadata: :key)
.where("(keys.name = ? AND metadata.value = ?) OR
(keys.name = ? AND metadata.value = ?)",
params[:key], params[:value], params[:key1], params[:value1])

Rails Arel equivalent of this complex sql query

Here is the original logic
(scrape_datas = ScrapeData.find(
:all, :conditions =>
"artist_status = 'NOT_FOUND'
AND blacklisted = 1
AND extracted = 0
and not EXISTS(
SELECT * FROM artist_name_suggestions where original = artist_name
)
I've been able to split up the first part better
scrape_datas = ScrapeData.where(
:artist_status => 'NOT_FOUND',
:blacklisted => 1,
:extracted => 0
)
Although having issues getting the "and not EXISTS" query into the mix
and not EXISTS(
SELECT * FROM artist_name_suggestions where original = artist_name
)
Thanks!
Firstly you can extract simple scopes:
scope :not_found, where(:artist_status => 'NOT_FOUND')
scope :blacklisted, where(:blacklisted => 1)
scope :extracted, where(:extracted => 0)
Then add a query method (assume artist_name is a column of scrape_datas):
def self.no_suggestions
scrape_datas = ScrapeData.arel_table
suggestions = ArtistNameSuggestion.arel_table
where(ArtistNameSuggestion.where(
suggestions[:original].eq(scrape_datas[:artist_name])
).exists.not)
end
Now you can do something like this:
ScrapeData.not_found.blacklisted.extracted.no_suggestions

ActiveRecord Arel OR condition

How can you combine 2 different conditions using logical OR instead of AND?
NOTE: 2 conditions are generated as rails scopes and can't be easily changed into something like where("x or y") directly.
Simple example:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
It's easy to apply AND condition (which for this particular case is meaningless):
(admins.merge authors).to_sql
#=> select ... from ... where kind = 'admin' AND kind = 'author'
But how can you produce the following query having 2 different Arel relations already available?
#=> select ... from ... where kind = 'admin' OR kind = 'author'
It seems (according to Arel readme):
The OR operator is not yet supported
But I hope it doesn't apply here and expect to write something like:
(admins.or authors).to_sql
ActiveRecord queries are ActiveRecord::Relation objects (which maddeningly do not support or), not Arel objects (which do).
[ UPDATE: as of Rails 5, "or" is supported in ActiveRecord::Relation; see https://stackoverflow.com/a/33248299/190135 ]
But luckily, their where method accepts ARel query objects. So if User < ActiveRecord::Base...
users = User.arel_table
query = User.where(users[:kind].eq('admin').or(users[:kind].eq('author')))
query.to_sql now shows the reassuring:
SELECT "users".* FROM "users" WHERE (("users"."kind" = 'admin' OR "users"."kind" = 'author'))
For clarity, you could extract some temporary partial-query variables:
users = User.arel_table
admin = users[:kind].eq('admin')
author = users[:kind].eq('author')
query = User.where(admin.or(author))
And naturally, once you have the query you can use query.all to execute the actual database call.
I'm a little late to the party, but here's the best suggestion I could come up with:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
admins = admins.where_values.reduce(:and)
authors = authors.where_values.reduce(:and)
User.where(admins.or(authors)).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE ((\"users\".\"kind\" = 'admin' OR \"users\".\"kind\" = 'author'))"
As of Rails 5 we have ActiveRecord::Relation#or, allowing you to do this:
User.where(kind: :author).or(User.where(kind: :admin))
...which gets translated into the sql you'd expect:
>> puts User.where(kind: :author).or(User.where(kind: :admin)).to_sql
SELECT "users".* FROM "users" WHERE ("users"."kind" = 'author' OR "users"."kind" = 'admin')
From the actual arel page:
The OR operator works like this:
users.where(users[:name].eq('bob').or(users[:age].lt(25)))
I've hit the same problem looking for an activerecord alternative to mongoid's #any_of.
#jswanner answer is good, but will only work if the where parameters are a Hash :
> User.where( email: 'foo', first_name: 'bar' ).where_values.reduce( :and ).method( :or )
=> #<Method: Arel::Nodes::And(Arel::Nodes::Node)#or>
> User.where( "email = 'foo' and first_name = 'bar'" ).where_values.reduce( :and ).method( :or )
NameError: undefined method `or' for class `String'
To be able to use both strings and hashes, you can use this :
q1 = User.where( "email = 'foo'" )
q2 = User.where( email: 'bar' )
User.where( q1.arel.constraints.reduce( :and ).or( q2.arel.constraints.reduce( :and ) ) )
Indeed, that's ugly, and you don't want to use that on a daily basis. Here is some #any_of implementation I've made : https://gist.github.com/oelmekki/5396826
It let do that :
> q1 = User.where( email: 'foo1' ); true
=> true
> q2 = User.where( "email = 'bar1'" ); true
=> true
> User.any_of( q1, q2, { email: 'foo2' }, "email = 'bar2'" )
User Load (1.2ms) SELECT "users".* FROM "users" WHERE (((("users"."email" = 'foo1' OR (email = 'bar1')) OR "users"."email" = 'foo2') OR (email = 'bar2')))
Edit : since then, I've published a gem to help building OR queries.
Just make a scope for your OR condition:
scope :author_or_admin, where(['kind = ? OR kind = ?', 'Author', 'Admin'])
Using SmartTuple it's going to look something like this:
tup = SmartTuple.new(" OR ")
tup << {:kind => "admin"}
tup << {:kind => "author"}
User.where(tup.compile)
OR
User.where((SmartTuple.new(" OR ") + {:kind => "admin"} + {:kind => "author"}).compile)
You may think I'm biased, but I still consider traditional data structure operations being far more clear and convenient than method chaining in this particular case.
To extend jswanner answer (which is actually awesome solution and helped me) for googling people:
you can apply scope like this
scope :with_owner_ids_or_global, lambda{ |owner_class, *ids|
with_ids = where(owner_id: ids.flatten).where_values.reduce(:and)
with_glob = where(owner_id: nil).where_values.reduce(:and)
where(owner_type: owner_class.model_name).where(with_ids.or( with_glob ))
}
User.with_owner_ids_or_global(Developer, 1, 2)
# => ...WHERE `users`.`owner_type` = 'Developer' AND ((`users`.`owner_id` IN (1, 2) OR `users`.`owner_id` IS NULL))
What about this approach: http://guides.rubyonrails.org/active_record_querying.html#hash-conditions (and check 2.3.3)
admins_or_authors = User.where(:kind => [:admin, :author])
Unfortunately it is not supported natively, so we need to hack here.
And the hack looks like this, which is pretty inefficient SQL (hope DBAs are not looking at it :-) ):
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
both = User.where("users.id in (#{admins.select(:id)}) OR users.id in (#{authors.select(:id)})")
both.to_sql # => where users.id in (select id from...) OR users.id in (select id from)
This generates subselets.
And a little better hack (from SQL perspective) looks like this:
admins_sql = admins.arel.where_sql.sub(/^WHERE/i,'')
authors_sql = authors.arel.where_sql.sub(/^WHERE/i,'')
both = User.where("(#{admins_sql}) OR (#{authors_sql})")
both.to_sql # => where <admins where conditions> OR <authors where conditions>
This generates proper OR condition, but obviously it only takes into account the WHERE part of the scopes.
I chose the 1st one until I'll see how it performs.
In any case, you must be pretty careful with it and watch the SQL generated.

Rails3/ActiveRecord: select sum with parameters?

Give the following (already simplified) query in SQLite:
def self.calculate(year, month, user_id, partner_id)
where(':user_id = entries.user_id OR :partner_id = entries.user_id', {
:user_id => user_id,
:partner_id => partner_id
}).
where('entries.date <= :last_day', {
:last_day => Date.new(year, month, 1).at_end_of_month
}).
select('sum(case when joint = "f" and user_id = :user_id then amount_calc else 0 end) as sum_single' , {
:user_id => user_id
}).
group("strftime('%Y-%m', date)")
end
The full query has more sums with different case when statements and some of them depend on whether it is user_id oder partner_id. Unfortunately, Rails complains as select does not take the second parameter with the substitutions like where does. Is there any way to achieve what I want without running two queries, one for user_id and one for partner_id?
One can be so blind....instead of:
select('sum(case when joint = "f" and user_id = :user_id then amount_calc else 0 end) as sum_single' , {
:user_id => user_id
}).
just build the string:
select('sum(case when joint = "f" and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_single').
As nobody answered, this is for the archives :)
Edit: Sorry, beware of that: as noted below, this is vulnerable.