Get Users with params and without others - sql

I'm trying to get group's users with specific ids that are not admin.
For the moment I have:
group.users
.joins(:roles)
.where(id: user_ids)
.where.not(roles: { role_type: Role::Type::ADMIN })
.pluck(:id)
In my log I have:
SQL to load the group:
(0.3ms) SELECT "users"."id" FROM "users" INNER JOIN "groups_users"
ON "users"."id" = "groups_users"."user_id"
WHERE "groups_users"."group_id" = $1 [["group_id", 137375]]
SQL for the query above:
(0.6ms) SELECT "users"."id" FROM "users" INNER JOIN "roles"
ON "roles"."user_id" = "users"."id" AND "roles"."is_destroyed" = $1
INNER JOIN "groups_users" ON "users"."id" = "groups_users"."user_id"
WHERE "groups_users"."group_id" = $2 AND "users"."id" IN (82884, 82885)
AND "roles"."role_type" != $3 [["is_destroyed", "f"],
["group_id", 137375], ["role_type", 1]]
The problem is I always get all the users of the group with matching user_ids. The where.not is not effective.
I had to do something like
users_in_group = group.users.where(id: user_ids).pluck(:id)
users_in_group -= group.users.joins(:roles).where
(roles: { role_type: Role::Type::ADMIN}).pluck(:id)
I don't understand why.

If you want to exclude Admins even if they have other roles, you might use SQL EXISTS:
group.users
.where(id: user_ids)
.where("NOT EXISTS (SELECT 1 FROM roles WHERE user_id = users.id AND role_type = ?", Role::Type::ADMIN)
.pluck(:id)
And, handling typical objection to such advice: it's perfectly fine to get your hands dirty by writing fragments of SQL when you are using ActiveRecord in Rails. You shouldn't limit yourself to the (not so broad) possibilities of its DSL.
UPD.
To simplify your code, you can use Where Exists gem (disclosure: I've written it recently).
Add gem 'where_exists' to your Gemfile, run bundle install, and then the following should work:
group.users
.where(id: user_ids)
.where_not_exists(:roles, role_type: Role::Type::ADMIN)
.pluck(:id)

Related

How to use joins with `where` condition

I want to get users with their assets, which have type 'Google'.
I tried
User.joins(:assets).where(assets: { assetable_type: 'Google' })
or
User.joins(:assets).where("assets.assetable_type = 'Google'")
But that scopes work identically and return nothing.
SQL, which scopes generate:
SELECT "users".* FROM "users" INNER JOIN "assets" ON "assets"."assetable_id" = "users"."id" AND "assets"."assetable_type" = 'User' WHERE (assets.assetable_type = 'Google')
It doesn't seem to look right
What am I doing wrong?
You didn't gave any details about your models and associations
So, I can give you a temporary solution to use raw sql
sql = "SELECT users.* FROM users INNER JOIN assets ON assets.assetable_id = users.id WHERE assets.assetable_type = 'Google'"
result = ActiveRecord::Base.connection.execute(sql).to_a
Note: If you are using relation as per rails standards it should be
sql = "SELECT users.* FROM users INNER JOIN assets ON assets.assetable_id = users.id WHERE assets.assetable_type = 'Google'"
result = ActiveRecord::Base.connection.execute(sql).to_a
But I think you are using another name i.e., assetable_id instead of user_id.

Check if a record is present in a has-many association

I have a rails-api application in which users can follow other users.
To check if an user already follows another user, I need to include a query in the attributes and because of that, I have always a N+1 query problem.
Here is my code:
Index action in user controller:
def index
#users = ::User.all.paginate(page: params[:page])
end
The followers will always be included by a default_scope in the User model.
index.json.jbuilder:
json.partial! 'attributes', collection: #users, as: :user
_attributes.json.jbuilder:
json.extract! user, :id, :firstname, :lastname, :username, :follower_count
is_follower = user.follower.find_by(id: current_user.id).present? if current_user
json.following is_follower
And as a result:
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = $1 [["followed_id", 14]]
Rendered v1/user/users/_attributes.json.jbuilder (1.3ms)
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = $1 [["followed_id", 9]]
Rendered v1/user/users/_attributes.json.jbuilder (1.4ms)
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = $1 [["followed_id", 13]]
Is there some workaround or is it somehow possible to generate a dynamic attribute in the SQL query which includes the boolean value if the user follows the other user?
Thank you very much in advance.
My first thought would be to eager load the followers using the .includes method when you get the list of users like this #users = ::User.all.includes(:followers).paginate(page: params[:page]). But perhaps, I'm not understanding your question correctly? Let me know if that works or if I should focus my answer on a different subject. Thanks!
EDIT: Correct answer from the comments below:
Perhaps you can try user.followers.include?(current_user) to make use of the pre-loaded followers association.

Make a request with activerecord to get only the users from groups and not from others

I'm trying to get users from few groups (with given ids) and exclude the users from other groups.
I've tried something like :
User.joins(:groups).where(groups: {id: ["8939","8950"]}).where.not(groups: {id: 8942}).map(&:id)
User Load (0.9ms) SELECT "users".* FROM "users" INNER JOIN "groups_users" ON "groups_users"."user_id" = "users"."id" INNER JOIN "groups" ON "groups"."id" = "groups_users"."group_id" WHERE "groups"."id" IN (8939, 8950) AND "groups"."id" != $1 [["id", 8942]]
=> [119491, 119489, 119490, 119492, 119488, 119484, 119483, 119491, 119482]
But that's not correct
The users in group 8942.
Group.find(8942).users.pluck(:id)
Group Load (0.4ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = $1 LIMIT 1 [["id", 8942]]
(0.6ms) SELECT "users"."id" FROM "users" INNER JOIN "groups_users" ON "users"."id" = "groups_users"."user_id" WHERE "groups_users"."group_id" = $1 [["group_id", 8942]]
=> [119490, 119492, 119491, 119457, 119423]
The where.not doesn't work on user "groups"."id" != $1 [["id", 8942]]. Why ?
Correct way to do such things is to use SQL EXISTS condition. I wish there was a specific ActiveRecord helper method for that, but there isn't at the moment.
Well, using pure SQL is just fine:
User.where("EXISTS (SELECT 1 FROM groups_users WHERE groups_users.user_id = users.id AND groups_users.group_id IN (?))", [8939, 8950]).
where("NOT EXISTS (SELECT 1 FROM groups_users WHERE groups_users.user_id = users.id AND groups_users.group_id IN (?))", [8942])
What you were doing with your original query is asking for not joining groups with [8942] ids to your query, and only joining groups with ids [8939, 8950]. Well, you can see right now that this doesn't make any sense: that's like asking to select every user whose name is bob and NOT charlie. Second condition doesn't add anything to the first one.
Join query is multiplicating columns, so if your user is in every group, result set would be:
user_id | group_id
1 | 8939
1 | 8950
1 | 8942
Then you filter out the latter row: 1 | 8942. Still, user 1 is in the result set and is returned.
And to ask the database to return only records which doesn't connect with another relation you should explicitly use NOT EXISTS which exists explicitly for that purpose :)
There is now a Where Exists gem which you can use. (Full disclosure: I've created that gem recently.)
With it you can achieve your task as simple as:
User.where_exists(:groups, id: [1, 2]).where_not_exists(:groups, id: [3, 4])

SQL Injection in ActiveRecord

I'm trying to build a vulnerable demo application. I'm using SQLite, and I have ruby code that looks like this:
#value = current_user.accounts.calculate(:sum, params[:column])
And the SQL generates the following by default:
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
(0.1ms) SELECT SUM("accounts"."account_value") AS sum_id FROM "accounts" WHERE "accounts"."user_id" = ? [["user_id", 1]]
Next, I put ssn) FROM users WHERE name = 'Texas'; -- into the form and I get the following:
(0.3ms) SELECT SUM(ssn) FROM users WHERE name = 'Texas'; --)) AS sum_id FROM "accounts" WHERE "accounts"."user_id" = ? [["user_id", 1]]
SQLite3::RangeException: bind or column index out of range: SELECT SUM(ssn) FROM users WHERE name = 'Texas'; --)) AS sum_id FROM "accounts" WHERE "accounts"."user_id" = ?
Completed 500 Internal Server Error in 2ms
ActiveRecord::StatementInvalid (SQLite3::RangeException: bind or column index out of range: SELECT SUM(ssn) FROM users WHERE name = 'Texas'; --)) AS sum_id FROM "accounts" WHERE "accounts"."user_id" = ?):
app/controllers/instant_calculator_controller.rb:3:in `sum'
I think the issue is that the 'user_id' section tacked onto the end as a paramiterized query is messing this up. I tried doing something like ssn) FROM users WHERE name = 'Texas'OR user_id = ?; -- just to throw that part of the query away, but that didn't seem to help.
Does anyone have any thoughts on I could make this work? I can change the code as well as the query, but I'd prefer to change the query before changing to code to make it SQLiable.
EDIT:
A bit more info. If I take the SQL that is generated and just change the last user_id to '1' so it looks like SELECT SUM(ssn) FROM users WHERE name = 'Texas'; --) AS sum_id FROM 'accounts' WHERE 'accounts'.'user_id' = 1 it works perfectly. I don't understand why this matters as everything after -- should be ignored.

Re-writing a SQL Query using Active Record or the Squeel Gem in Ruby on Rails 4

I am having a lot of problems rewriting this SQL Query in Squeel or straight Active Record using Ruby on Rails 4.
All 3 numbers in the query need to be passed to the query before execution. The SQL Query is below.
SELECT "users".*
FROM "users"
WHERE "users"."id"
IN (SELECT "users"."id" FROM "users"
INNER JOIN marketing_assets ON users.id = marketing_assets.user_id
WHERE marketing_assets.marketing_platform_id= 3
GROUP BY users.id HAVING COUNT(DISTINCT marketing_assets.marketing_platform_id) = 1)
You can find out more detail about how it is being used at Error when trying to chain class method in controller in Ruby on Rails
marketing_assets_id = 3
limit_marketing_assets = 1
User.joins(:marketing_assets).where(:marketing_assets => {:marketing_platform_id => marketing_assets_id}).group(:users => :id).having("COUNT(DISTINCT marketing_assets.marketing_platform_id) = #{limit_marketing_assets}")
Here is the query I ended up writing using Squeel. It works for me.
User.where{users.id.in(User.joins{marketing_assets}
.where((['marketing_assets.marketing_platform_id = ?' ] * platforms.count)
.join(' OR '), *platforms).group{users.id}
.having{{marketing_assets =>
{COUNT(DISTINCT(marketing_assets.marketing_platform_id)) => platforms.count }}})}